diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py index 74b064a555..381f266fab 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py @@ -148,7 +148,7 @@ class TestAllComponentsIndepthTests(object): golden_image_path = os.path.join(golden_images_directory(), golden_image) golden_images.append(golden_image_path) - expected_lines = ["Light component tests completed."] + expected_lines = ["spot_light Controller|Configuration|Shadows|Shadowmap size: SUCCESS"] unexpected_lines = [ "Trace::Assert", "Trace::Error", diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_Optimized.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_Optimized.py index 31b4cb687d..447a4ebac9 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_Optimized.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_Optimized.py @@ -67,5 +67,9 @@ class TestAutomation(EditorTestSuite): class AtomEditorComponents_PostFXLayerAdded(EditorSharedTest): from Atom.tests import hydra_AtomEditorComponents_PostFXLayerAdded as test_module + @pytest.mark.test_case_id("C36525665") + class AtomEditorComponents_PostFXShapeWeightModifierAdded(EditorSharedTest): + from Atom.tests import hydra_AtomEditorComponents_PostFxShapeWeightModifierAdded as test_module + class ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(EditorSharedTest): from Atom.tests import hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges as test_module diff --git a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_PostFxShapeWeightModifierAdded.py b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_PostFxShapeWeightModifierAdded.py new file mode 100644 index 0000000000..4c98bcd130 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_PostFxShapeWeightModifierAdded.py @@ -0,0 +1,207 @@ +""" +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 +""" + +class Tests: + creation_undo = ( + "UNDO Entity creation success", + "UNDO Entity creation failed") + creation_redo = ( + "REDO Entity creation success", + "REDO Entity creation failed") + postfx_shape_weight_creation = ( + "PostFx Shape Weight Modifier Entity successfully created", + "PostFx Shape Weight Modifier Entity failed to be created") + postfx_shape_weight_component = ( + "Entity has a PostFx Shape Weight Modifier component", + "Entity failed to find PostFx Shape Weight Modifier component") + postfx_shape_weight_disabled = ( + "PostFx Shape Weight Modifier component disabled", + "PostFx Shape Weight Modifier component was not disabled.") + postfx_layer_component = ( + "Entity has a PostFX Layer component", + "Entity did not have an PostFX Layer component") + tube_shape_component = ( + "Entity has a Tube Shape component", + "Entity did not have a Tube Shape component") + postfx_shape_weight_enabled = ( + "PostFx Shape Weight Modifier component enabled", + "PostFx Shape Weight Modifier component was not enabled.") + enter_game_mode = ( + "Entered game mode", + "Failed to enter game mode") + exit_game_mode = ( + "Exited game mode", + "Couldn't exit game mode") + is_visible = ( + "Entity is visible", + "Entity was not visible") + is_hidden = ( + "Entity is hidden", + "Entity was not hidden") + entity_deleted = ( + "Entity deleted", + "Entity was not deleted") + deletion_undo = ( + "UNDO deletion success", + "UNDO deletion failed") + deletion_redo = ( + "REDO deletion success", + "REDO deletion failed") + + +def AtomEditorComponents_postfx_shape_weight_AddedToEntity(): + """ + Summary: + Tests the PostFx Shape Weight Modifier component can be added to an entity and has the expected functionality. + + Test setup: + - Wait for Editor idle loop. + - Open the "Base" level. + + Expected Behavior: + The component can be added, used in game mode, hidden/shown, deleted, and has accurate required components. + Creation and deletion undo/redo should also work. + + Test Steps: + 1) Create a PostFx Shape Weight Modifier entity with no components. + 2) Add a PostFx Shape Weight Modifier component to PostFx Shape Weight Modifier entity. + 3) UNDO the entity creation and component addition. + 4) REDO the entity creation and component addition. + 5) Verify PostFx Shape Weight Modifier component not enabled. + 6) Add PostFX Layer component since it is required by the PostFx Shape Weight Modifier component. + 7) Verify PostFx Shape Weight Modifier component is NOT enabled since it also requires a shape. + 8) Add a required shape looping over a list and checking if it enables PostFX Shape Weight Modifier. + 9) Undo to remove each added shape and verify PostFX Shape Weight Modifier is not enabled. + 10) Verify PostFx Shape Weight Modifier component is enabled by adding Spline and Tube Shape component. + 11) Enter/Exit game mode. + 12) Test IsHidden. + 13) Test IsVisible. + 14) Delete PostFx Shape Weight Modifier entity. + 15) UNDO deletion. + 16) REDO deletion. + 17) Look for errors. + + :return: None + """ + + import azlmbr.legacy.general as general + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.utils import Report, Tracer, TestHelper + + with Tracer() as error_tracer: + # Test setup begins. + # Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level. + TestHelper.init_idle() + TestHelper.open_level("", "Base") + + # Test steps begin. + # 1. Create a PostFx Shape Weight Modifier entity with no components. + postfx_shape_weight_name = "PostFX Shape Weight Modifier" + postfx_shape_weight_entity = EditorEntity.create_editor_entity(postfx_shape_weight_name) + Report.critical_result(Tests.postfx_shape_weight_creation, postfx_shape_weight_entity.exists()) + + # 2. Add a PostFx Shape Weight Modifier component to PostFx Shape Weight Modifier entity. + postfx_shape_weight_component = postfx_shape_weight_entity.add_component(postfx_shape_weight_name) + Report.critical_result( + Tests.postfx_shape_weight_component, + postfx_shape_weight_entity.has_component(postfx_shape_weight_name)) + + # 3. UNDO the entity creation and component addition. + # -> UNDO component addition. + general.undo() + # -> UNDO naming entity. + general.undo() + # -> UNDO selecting entity. + general.undo() + # -> UNDO entity creation. + general.undo() + general.idle_wait_frames(1) + Report.result(Tests.creation_undo, not postfx_shape_weight_entity.exists()) + + # 4. REDO the entity creation and component addition. + # -> REDO entity creation. + general.redo() + # -> REDO selecting entity. + general.redo() + # -> REDO naming entity. + general.redo() + # -> REDO component addition. + general.redo() + general.idle_wait_frames(1) + Report.result(Tests.creation_redo, postfx_shape_weight_entity.exists()) + + # 5. Verify PostFx Shape Weight Modifier component not enabled. + Report.result(Tests.postfx_shape_weight_disabled, not postfx_shape_weight_component.is_enabled()) + + # 6. Add PostFX Layer component since it is required by the PostFx Shape Weight Modifier component. + postfx_layer_name = "PostFX Layer" + postfx_shape_weight_entity.add_component(postfx_layer_name) + Report.result(Tests.postfx_layer_component, postfx_shape_weight_entity.has_component(postfx_layer_name)) + + # 7. Verify PostFx Shape Weight Modifier component is NOT enabled since it also requires a shape. + Report.result(Tests.postfx_shape_weight_disabled, not postfx_shape_weight_component.is_enabled()) + + # 8. Add a required shape looping over a list and checking if it enables PostFX Shape Weight Modifier. + for shape in ['Axis Aligned Box Shape', 'Box Shape', 'Capsule Shape', 'Compound Shape', 'Cylinder Shape', + 'Disk Shape', 'Polygon Prism Shape', 'Quad Shape', 'Sphere Shape', 'Vegetation Reference Shape']: + postfx_shape_weight_entity.add_component(shape) + test_shape = ( + f"Entity has a {shape} component", + f"Entity did not have a {shape} component") + Report.result(test_shape, postfx_shape_weight_entity.has_component(shape)) + + # Check if required shape allows PostFX Shape Weight Modifier to be enabled + Report.result(Tests.postfx_shape_weight_enabled, postfx_shape_weight_component.is_enabled()) + + # 9. Undo to remove each added shape and verify PostFX Shape Weight Modifier is not enabled. + general.undo() + TestHelper.wait_for_condition(lambda: not postfx_shape_weight_entity.has_component(shape), 1.0) + Report.result(Tests.postfx_shape_weight_disabled, not postfx_shape_weight_component.is_enabled()) + + # 10. Verify PostFx Shape Weight Modifier component is enabled by adding Spline and Tube Shape component. + postfx_shape_weight_entity.add_components(['Spline', 'Tube Shape']) + Report.result(Tests.tube_shape_component, postfx_shape_weight_entity.has_component('Tube Shape')) + Report.result(Tests.postfx_shape_weight_enabled, postfx_shape_weight_component.is_enabled()) + + # 11. Enter/Exit game mode. + TestHelper.enter_game_mode(Tests.enter_game_mode) + general.idle_wait_frames(1) + TestHelper.exit_game_mode(Tests.exit_game_mode) + + # 12. Test IsHidden. + postfx_shape_weight_entity.set_visibility_state(False) + Report.result(Tests.is_hidden, postfx_shape_weight_entity.is_hidden() is True) + + # 13. Test IsVisible. + postfx_shape_weight_entity.set_visibility_state(True) + general.idle_wait_frames(1) + Report.result(Tests.is_visible, postfx_shape_weight_entity.is_visible() is True) + + # 14. Delete PostFx Shape Weight Modifier entity. + postfx_shape_weight_entity.delete() + Report.result(Tests.entity_deleted, not postfx_shape_weight_entity.exists()) + + # 15. UNDO deletion. + general.undo() + Report.result(Tests.deletion_undo, postfx_shape_weight_entity.exists()) + + # 16. REDO deletion. + general.redo() + Report.result(Tests.deletion_redo, not postfx_shape_weight_entity.exists()) + + # 17. Look for errors or asserts. + TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0) + for error_info in error_tracer.errors: + Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}") + for assert_info in error_tracer.asserts: + Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}") + + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(AtomEditorComponents_postfx_shape_weight_AddedToEntity) diff --git a/AutomatedTesting/Objects/MorphTargets/DisplayWrinkleMaskBlendValues.material b/AutomatedTesting/Objects/MorphTargets/DisplayWrinkleMaskBlendValues.material new file mode 100644 index 0000000000..878b3ac39f --- /dev/null +++ b/AutomatedTesting/Objects/MorphTargets/DisplayWrinkleMaskBlendValues.material @@ -0,0 +1,13 @@ +{ + "description": "", + "materialType": "Materials/Types/Skin.materialtype", + "parentMaterial": "", + "propertyLayoutVersion": 3, + "properties": { + "wrinkleLayers": { + "count": 3, + "enable": true, + "showBlendValues": true + } + } +} diff --git a/AutomatedTesting/Objects/MorphTargets/morphActor.fbx b/AutomatedTesting/Objects/MorphTargets/morphActor.fbx new file mode 100644 index 0000000000..ffb75e680e --- /dev/null +++ b/AutomatedTesting/Objects/MorphTargets/morphActor.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53e17ec8155911c8b42e85436130f600bd6dddd8931a8ccb1b2f8a9f8674cc85 +size 45104 diff --git a/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph1_wrinklemask.tif b/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph1_wrinklemask.tif new file mode 100644 index 0000000000..3bc18cf450 --- /dev/null +++ b/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph1_wrinklemask.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0da56a05daa0ec1c476cfe25ca6d3b65267c98886cf33408f6e852fb325a8e2c +size 198084 diff --git a/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph2_wriklemask.tif b/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph2_wriklemask.tif new file mode 100644 index 0000000000..39e70f5acf --- /dev/null +++ b/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph2_wriklemask.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3537fbe9205731a242251c525a67bbb5f3b8f5307537f1dc0c318b5b885ce52 +size 198112 diff --git a/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph3_wrinklemask.tif b/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph3_wrinklemask.tif new file mode 100644 index 0000000000..43e19f0d5f --- /dev/null +++ b/AutomatedTesting/Objects/MorphTargets/morphActor_wrinklemasks/Morph3_wrinklemask.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd794d5dd4b749c3275bfab79b9b5ae3f8e007d3e6741c0566c9c2d3931123bf +size 198112 diff --git a/AutomatedTesting/Objects/MorphTargets/morphAnimation.fbx b/AutomatedTesting/Objects/MorphTargets/morphAnimation.fbx new file mode 100644 index 0000000000..c0dcc007dc --- /dev/null +++ b/AutomatedTesting/Objects/MorphTargets/morphAnimation.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45ded862987a64061deffd8e4c9aa1dff4eec3bcff5f7b505679f1959e8ae137 +size 51440 diff --git a/Code/Framework/AzFramework/AzFramework/Gem/GemInfo.h b/Code/Framework/AzFramework/AzFramework/Gem/GemInfo.h index 1f300af770..3435d810fd 100644 --- a/Code/Framework/AzFramework/AzFramework/Gem/GemInfo.h +++ b/Code/Framework/AzFramework/AzFramework/Gem/GemInfo.h @@ -29,6 +29,10 @@ namespace AzFramework AZStd::vector m_absoluteSourcePaths; //!< Where the gem's source path folder are located(as an absolute path) static constexpr const char* GetGemAssetFolder() { return "Assets"; } + static constexpr const char* GetGemRegistryFolder() + { + return "Registry"; + } }; //! Returns a list of GemInfo of all the gems that are active for the for the specified game project. diff --git a/Code/Framework/AzFramework/AzFramework/Input/System/InputSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Input/System/InputSystemComponent.cpp index 33690f2246..14fb7ea5eb 100644 --- a/Code/Framework/AzFramework/AzFramework/Input/System/InputSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Input/System/InputSystemComponent.cpp @@ -20,6 +20,7 @@ #include #include #include +#include //////////////////////////////////////////////////////////////////////////////////////////////////// namespace AzFramework @@ -190,6 +191,25 @@ namespace AzFramework //////////////////////////////////////////////////////////////////////////////////////////////// void InputSystemComponent::Activate() { + const auto* settingsRegistry = AZ::SettingsRegistry::Get(); + if (settingsRegistry) + { + AZ::u64 value = 0; + if (settingsRegistry->Get(value, "/O3DE/InputSystem/MouseMovementSampleRateHertz")) + { + m_mouseMovementSampleRateHertz = aznumeric_caster(value); + } + if (settingsRegistry->Get(value, "/O3DE/InputSystem/GamepadsEnabled")) + { + m_gamepadsEnabled = aznumeric_caster(value); + } + settingsRegistry->Get(m_keyboardEnabled, "/O3DE/InputSystem/KeyboardEnabled"); + settingsRegistry->Get(m_motionEnabled, "/O3DE/InputSystem/MotionEnabled"); + settingsRegistry->Get(m_mouseEnabled, "/O3DE/InputSystem/MouseEnabled"); + settingsRegistry->Get(m_touchEnabled, "/O3DE/InputSystem/TouchEnabled"); + settingsRegistry->Get(m_virtualKeyboardEnabled, "/O3DE/InputSystem/VirtualKeyboardEnabled"); + } + // Create all enabled input devices CreateEnabledInputDevices(); diff --git a/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.cpp b/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.cpp index 0c879a930f..0856ddeed6 100644 --- a/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.cpp +++ b/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.cpp @@ -22,6 +22,7 @@ namespace AzFramework ->Field("terminationTime", &SessionConfig::m_terminationTime) ->Field("creatorId", &SessionConfig::m_creatorId) ->Field("sessionProperties", &SessionConfig::m_sessionProperties) + ->Field("matchmakingData", &SessionConfig::m_matchmakingData) ->Field("sessionId", &SessionConfig::m_sessionId) ->Field("sessionName", &SessionConfig::m_sessionName) ->Field("dnsName", &SessionConfig::m_dnsName) @@ -46,6 +47,8 @@ namespace AzFramework "CreatorId", "A unique identifier for a player or entity creating the session.") ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionProperties, "SessionProperties", "A collection of custom properties for a session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_matchmakingData, + "MatchmakingData", "The matchmaking process information that was used to create the session.") ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionId, "SessionId", "A unique identifier for the session.") ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionName, diff --git a/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.h b/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.h index cfd6aa7c8b..45e40c2f29 100644 --- a/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.h +++ b/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.h @@ -35,6 +35,9 @@ namespace AzFramework // A collection of custom properties for a session. AZStd::unordered_map m_sessionProperties; + + // The matchmaking process information that was used to create the session. + AZStd::string m_matchmakingData; // A unique identifier for the session. AZStd::string m_sessionId; diff --git a/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h b/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h index 7788c2d030..902500fe9a 100644 --- a/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h +++ b/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h @@ -41,6 +41,11 @@ namespace AzFramework // OnDestroySessionBegin is fired at the beginning of session termination // @return The result of all OnDestroySessionBegin notifications virtual bool OnDestroySessionBegin() = 0; + + // OnUpdateSessionBegin is fired at the beginning of session update + // @param sessionConfig The properties to describe a session + // @param updateReason The reason for session update + virtual void OnUpdateSessionBegin(const SessionConfig& sessionConfig, const AZStd::string& updateReason) = 0; }; using SessionNotificationBus = AZ::EBus; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp index 6ab97fd616..43f7e7140f 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp index 2b1f21ede7..76d00eda50 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceKeyboardTests.cpp @@ -12,7 +12,6 @@ #include -#include #include #include #include @@ -20,6 +19,7 @@ #include "Matchers.h" #include "Actions.h" #include "XcbBaseTestFixture.h" +#include "XcbTestApplication.h" template xcb_generic_event_t MakeEvent(T event) @@ -33,6 +33,7 @@ namespace AzFramework class XcbInputDeviceKeyboardTests : public XcbBaseTestFixture { + public: void SetUp() override { using testing::Return; @@ -123,6 +124,15 @@ namespace AzFramework static constexpr xcb_keycode_t s_keycodeForAKey{38}; static constexpr xcb_keycode_t s_keycodeForShiftLKey{50}; + + XcbTestApplication m_application{ + /*enabledGamepadsCount=*/0, + /*keyboardEnabled=*/true, + /*motionEnabled=*/false, + /*mouseEnabled=*/false, + /*touchEnabled=*/false, + /*virtualKeyboardEnabled=*/false + }; }; class InputTextNotificationListener @@ -195,27 +205,23 @@ namespace AzFramework EXPECT_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForAKey)) .Times(2); - Application application; - application.Start({}, {}); - AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize); + m_application.Start(); const InputChannel* inputChannel = InputChannelRequests::FindInputChannel(InputDeviceKeyboard::Key::AlphanumericA); ASSERT_TRUE(inputChannel); EXPECT_THAT(inputChannel->GetState(), Eq(InputChannel::State::Idle)); - application.PumpSystemEventLoopUntilEmpty(); - application.TickSystem(); - application.Tick(); + m_application.PumpSystemEventLoopUntilEmpty(); + m_application.TickSystem(); + m_application.Tick(); EXPECT_THAT(inputChannel->GetState(), Eq(InputChannel::State::Began)); - application.PumpSystemEventLoopUntilEmpty(); - application.TickSystem(); - application.Tick(); + m_application.PumpSystemEventLoopUntilEmpty(); + m_application.TickSystem(); + m_application.Tick(); EXPECT_THAT(inputChannel->GetState(), Eq(InputChannel::State::Ended)); - - application.Stop(); } TEST_F(XcbInputDeviceKeyboardTests, TextEnteredFromXcbKeyPressEvents) @@ -420,17 +426,13 @@ namespace AzFramework EXPECT_CALL(textListener, OnInputTextEvent(StrEq("a"), _)).Times(1); EXPECT_CALL(textListener, OnInputTextEvent(StrEq("A"), _)).Times(1); - Application application; - application.Start({}, {}); - AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize); + m_application.Start(); for (int i = 0; i < 4; ++i) { - application.PumpSystemEventLoopUntilEmpty(); - application.TickSystem(); - application.Tick(); + m_application.PumpSystemEventLoopUntilEmpty(); + m_application.TickSystem(); + m_application.Tick(); } - - application.Stop(); } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbTestApplication.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbTestApplication.h new file mode 100644 index 0000000000..3035a9de74 --- /dev/null +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbTestApplication.h @@ -0,0 +1,38 @@ +/* + * 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 +#include +#include + +namespace AzFramework +{ + class XcbTestApplication + : public Application + { + public: + XcbTestApplication(AZ::u64 enabledGamepadsCount, bool keyboardEnabled, bool motionEnabled, bool mouseEnabled, bool touchEnabled, bool virtualKeyboardEnabled) + { + auto* settingsRegistry = AZ::SettingsRegistry::Get(); + settingsRegistry->Set("/O3DE/InputSystem/GamepadsEnabled", enabledGamepadsCount); + settingsRegistry->Set("/O3DE/InputSystem/KeyboardEnabled", keyboardEnabled); + settingsRegistry->Set("/O3DE/InputSystem/MotionEnabled", motionEnabled); + settingsRegistry->Set("/O3DE/InputSystem/MouseEnabled", mouseEnabled); + settingsRegistry->Set("/O3DE/InputSystem/TouchEnabled", touchEnabled); + settingsRegistry->Set("/O3DE/InputSystem/VirtualKeyboardEnabled", virtualKeyboardEnabled); + } + + void Start(const Descriptor& descriptor = {}, const StartupParameters& startupParameters = {}) override + { + Application::Start(descriptor, startupParameters); + AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize); + } + }; +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake index 762d5d74fe..147fd2bfe1 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/azframework_xcb_tests_files.cmake @@ -17,4 +17,5 @@ set(FILES XcbBaseTestFixture.cpp XcbBaseTestFixture.h XcbInputDeviceKeyboardTests.cpp + XcbTestApplication.h ) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Linux/platform_linux_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Linux/platform_linux_files.cmake index ec71d7b508..f292c29c3c 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Linux/platform_linux_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Linux/platform_linux_files.cmake @@ -12,6 +12,7 @@ set(FILES ../../Utilities/QtWindowUtilities_linux.cpp ../../Utilities/ScreenGrabber_linux.cpp ../../../Platform/Linux/AzQtComponents/Components/StyledDockWidget_Linux.cpp + ../../../Platform/Linux/AzQtComponents/Utilities/DesktopUtilities_Linux.cpp ../../../Platform/Linux/AzQtComponents/AzQtComponents_Traits_Linux.h ../../../Platform/Linux/AzQtComponents/AzQtComponents_Traits_Platform.h ) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Mac/platform_mac_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Mac/platform_mac_files.cmake index 18f9479289..4addf53599 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Mac/platform_mac_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Mac/platform_mac_files.cmake @@ -12,6 +12,7 @@ set(FILES ../../Utilities/QtWindowUtilities_mac.mm ../../Utilities/ScreenGrabber_mac.mm ../../../Platform/Mac/AzQtComponents/Components/StyledDockWidget_Mac.cpp + ../../../Platform/Mac/AzQtComponents/Utilities/DesktopUtilities_Mac.cpp ../../../Platform/Mac/AzQtComponents/AzQtComponents_Traits_Mac.h ../../../Platform/Mac/AzQtComponents/AzQtComponents_Traits_Platform.h ) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Windows/platform_windows_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Windows/platform_windows_files.cmake index ca4145ef35..bf0d0bb785 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Windows/platform_windows_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Windows/platform_windows_files.cmake @@ -9,6 +9,7 @@ set(FILES ../../natvis/qt.natvis ../../../Platform/Windows/AzQtComponents/Utilities/HandleDpiAwareness_Windows.cpp + ../../../Platform/Windows/AzQtComponents/Utilities/DesktopUtilities_Windows.cpp ../../Utilities/MouseHider_win.cpp ../../Utilities/QtWindowUtilities_win.cpp ../../Utilities/ScreenGrabber_win.cpp diff --git a/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake index 8219ab04b2..4c343b852c 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake @@ -271,7 +271,6 @@ set(FILES Utilities/ColorUtilities.h Utilities/Conversions.h Utilities/Conversions.cpp - Utilities/DesktopUtilities.cpp Utilities/DesktopUtilities.h Utilities/HandleDpiAwareness.cpp Utilities/HandleDpiAwareness.h diff --git a/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/Utilities/DesktopUtilities_Linux.cpp b/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/Utilities/DesktopUtilities_Linux.cpp new file mode 100644 index 0000000000..e4ddaceec7 --- /dev/null +++ b/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/Utilities/DesktopUtilities_Linux.cpp @@ -0,0 +1,49 @@ +/* + * 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 + +#include +#include + +namespace AzQtComponents +{ + void ShowFileOnDesktop(const QString& path) + { + const char* defaultNautilusPath = "/usr/bin/nautilus"; + const char* defaultXdgOpenPath = "/usr/bin/xdg-open"; + + // Determine if Nautilus (for Gnome Desktops) is available because it supports opening the file manager + // and selecting a specific file + bool nautilusAvailable = QFileInfo(defaultNautilusPath).exists(); + + QFileInfo pathInfo(path); + if (pathInfo.isDir()) + { + QProcess::startDetached(defaultXdgOpenPath, { path }); + } + else + { + if (nautilusAvailable) + { + QProcess::startDetached(defaultNautilusPath, { "--select", path }); + } + else + { + QDir parentDir { pathInfo.dir() }; + QProcess::startDetached(defaultXdgOpenPath, { parentDir.path() }); + } + } + } + + QString fileBrowserActionName() + { + const char* exploreActionName = "Open in file browser"; + return QObject::tr(exploreActionName); + } +} diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Utilities/DesktopUtilities.cpp b/Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/Utilities/DesktopUtilities_Mac.cpp similarity index 67% rename from Code/Framework/AzQtComponents/AzQtComponents/Utilities/DesktopUtilities.cpp rename to Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/Utilities/DesktopUtilities_Mac.cpp index c210f17136..5920dc76c6 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Utilities/DesktopUtilities.cpp +++ b/Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/Utilities/DesktopUtilities_Mac.cpp @@ -15,21 +15,6 @@ namespace AzQtComponents { void ShowFileOnDesktop(const QString& path) { -#if defined(AZ_PLATFORM_WINDOWS) - - // Launch explorer at the path provided - QStringList args; - if (!QFileInfo(path).isDir()) - { - // Folders are just opened, files are selected - args << "/select,"; - } - args << QDir::toNativeSeparators(path); - - QProcess::startDetached("explorer", args); - -#else - if (QFileInfo(path).isDir()) { QProcess::startDetached("/usr/bin/osascript", { "-e", @@ -43,19 +28,11 @@ namespace AzQtComponents QProcess::startDetached("/usr/bin/osascript", { "-e", QStringLiteral("tell application \"Finder\" to activate") }); - -#endif } QString fileBrowserActionName() { -#ifdef AZ_PLATFORM_WINDOWS - const char* exploreActionName = "Open in Explorer"; -#elif defined(AZ_PLATFORM_MAC) const char* exploreActionName = "Open in Finder"; -#else - const char* exploreActionName = "Open in file browser"; -#endif return QObject::tr(exploreActionName); } } diff --git a/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/Utilities/DesktopUtilities_Windows.cpp b/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/Utilities/DesktopUtilities_Windows.cpp new file mode 100644 index 0000000000..796e2c0ccf --- /dev/null +++ b/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/Utilities/DesktopUtilities_Windows.cpp @@ -0,0 +1,35 @@ +/* + * 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 + +#include +#include + +namespace AzQtComponents +{ + void ShowFileOnDesktop(const QString& path) + { + // Launch explorer at the path provided + QStringList args; + if (!QFileInfo(path).isDir()) + { + // Folders are just opened, files are selected + args << "/select,"; + } + args << QDir::toNativeSeparators(path); + + QProcess::startDetached("explorer", args); + } + + QString fileBrowserActionName() + { + const char* exploreActionName = "Open in Explorer"; + return QObject::tr(exploreActionName); + } +} diff --git a/Code/Tools/AssetProcessor/native/tests/platformconfiguration/platformconfigurationtests.cpp b/Code/Tools/AssetProcessor/native/tests/platformconfiguration/platformconfigurationtests.cpp index 6f07901e27..69397745f1 100644 --- a/Code/Tools/AssetProcessor/native/tests/platformconfiguration/platformconfigurationtests.cpp +++ b/Code/Tools/AssetProcessor/native/tests/platformconfiguration/platformconfigurationtests.cpp @@ -590,20 +590,22 @@ TEST_F(PlatformConfigurationUnitTests, Test_GemHandling) AssetUtilities::ResetAssetRoot(); - ASSERT_EQ(2, config.GetScanFolderCount()); + ASSERT_EQ(4, config.GetScanFolderCount()); EXPECT_FALSE(config.GetScanFolderAt(0).IsRoot()); EXPECT_TRUE(config.GetScanFolderAt(0).RecurseSubFolders()); // the first one is a game gem, so its order should be above 1 but below 100. EXPECT_GE(config.GetScanFolderAt(0).GetOrder(), 100); EXPECT_EQ(0, config.GetScanFolderAt(0).ScanPath().compare(expectedScanFolder, Qt::CaseInsensitive)); - // for each gem, there are currently 1 scan folder, the gem assets folder, with no output prefix + // for each gem, there are currently 2 scan folders: + // The Gem's 'Assets' folder + // The Gem's 'Registry' folder expectedScanFolder = tempPath.absoluteFilePath("Gems/LmbrCentral/v2/Assets"); - EXPECT_FALSE(config.GetScanFolderAt(1).IsRoot() ); - EXPECT_TRUE(config.GetScanFolderAt(1).RecurseSubFolders()); - EXPECT_GT(config.GetScanFolderAt(1).GetOrder(), config.GetScanFolderAt(0).GetOrder()); - EXPECT_EQ(0, config.GetScanFolderAt(1).ScanPath().compare(expectedScanFolder, Qt::CaseInsensitive)); + EXPECT_FALSE(config.GetScanFolderAt(2).IsRoot() ); + EXPECT_TRUE(config.GetScanFolderAt(2).RecurseSubFolders()); + EXPECT_GT(config.GetScanFolderAt(2).GetOrder(), config.GetScanFolderAt(0).GetOrder()); + EXPECT_EQ(0, config.GetScanFolderAt(2).ScanPath().compare(expectedScanFolder, Qt::CaseInsensitive)); } TEST_F(PlatformConfigurationUnitTests, Test_MetaFileTypes) diff --git a/Code/Tools/AssetProcessor/native/utilities/PlatformConfiguration.cpp b/Code/Tools/AssetProcessor/native/utilities/PlatformConfiguration.cpp index 52df8e901d..4a033cf6ca 100644 --- a/Code/Tools/AssetProcessor/native/utilities/PlatformConfiguration.cpp +++ b/Code/Tools/AssetProcessor/native/utilities/PlatformConfiguration.cpp @@ -1582,6 +1582,24 @@ namespace AssetProcessor gemOrder, /*scanFolderId*/ 0, /*canSaveNewAssets*/ true)); // Users can create assets like slices in Gem asset folders. + + // Now add another scan folder on Gem/GemName/Registry... + gemFolder = gemDir.absoluteFilePath(AzFramework::GemInfo::GetGemRegistryFolder()); + gemFolder = AssetUtilities::NormalizeDirectoryPath(gemFolder); + + assetBrowserDisplayName = AzFramework::GemInfo::GetGemRegistryFolder(); + portableKey = QString("gemregistry-%1").arg(gemNameAsUuid); + gemOrder++; + + AZ_TracePrintf(AssetProcessor::DebugChannel, "Adding GEM registry folder for monitoring / scanning: %s.\n", gemFolder.toUtf8().data()); + AddScanFolder(ScanFolderInfo( + gemFolder, + assetBrowserDisplayName, + portableKey, + isRoot, + isRecursive, + platforms, + gemOrder)); } } } diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/CMakeLists.txt b/Gems/AWSGameLift/Code/AWSGameLiftClient/CMakeLists.txt index 3eb5eafab0..1fab09e9f4 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/CMakeLists.txt +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/CMakeLists.txt @@ -15,10 +15,11 @@ ly_add_target( awsgamelift_client_files.cmake INCLUDE_DIRECTORIES PUBLIC + ../AWSGameLiftCommon/Include Include PRIVATE - Source ../AWSGameLiftCommon/Source + Source COMPILE_DEFINITIONS PRIVATE ${awsgameliftclient_compile_definition} @@ -78,10 +79,11 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) awsgamelift_client_tests_files.cmake INCLUDE_DIRECTORIES PRIVATE + ../AWSGameLiftCommon/Include + ../AWSGameLiftCommon/Source Include Tests Source - ../AWSGameLiftCommon/Source BUILD_DEPENDENCIES PRIVATE AZ::AzCore diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftStartMatchmakingRequest.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftStartMatchmakingRequest.h index d734daa808..ec3719c6d3 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftStartMatchmakingRequest.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftStartMatchmakingRequest.h @@ -10,36 +10,12 @@ #include #include - #include +#include + namespace AWSGameLift { - //! AWSGameLiftPlayerInformation - //! Information on each player to be matched - //! This information must include a player ID, and may contain player attributes and latency data to be used in the matchmaking process - //! After a successful match, Player objects contain the name of the team the player is assigned to - struct AWSGameLiftPlayerInformation - { - AZ_RTTI(AWSGameLiftPlayerInformation, "{B62C118E-C55D-4903-8ECB-E58E8CA613C4}"); - static void Reflect(AZ::ReflectContext* context); - - AWSGameLiftPlayerInformation() = default; - virtual ~AWSGameLiftPlayerInformation() = default; - - // A map of region names to latencies in millseconds, that indicates - // the amount of latency that a player experiences when connected to AWS Regions - AZStd::unordered_map m_latencyInMs; - // A collection of key:value pairs containing player information for use in matchmaking - // Player attribute keys must match the playerAttributes used in a matchmaking rule set - // Example: {"skill": "{\"N\": \"23\"}", "gameMode": "{\"S\": \"deathmatch\"}"} - AZStd::unordered_map m_playerAttributes; - // A unique identifier for a player - AZStd::string m_playerId; - // Name of the team that the player is assigned to in a match - AZStd::string m_team; - }; - //! AWSGameLiftStartMatchmakingRequest //! GameLift start matchmaking request which corresponds to Amazon GameLift //! Uses FlexMatch to create a game match for a group of players based on custom matchmaking rules @@ -57,6 +33,6 @@ namespace AWSGameLift // Name of the matchmaking configuration to use for this request AZStd::string m_configurationName; // Information on each player to be matched - AZStd::vector m_players; + AZStd::vector m_players; }; } // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp index ea5fff99a3..ed7830ce57 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp @@ -210,6 +210,7 @@ namespace AWSGameLift ->Property("SessionId", BehaviorValueProperty(&AzFramework::SessionConfig::m_sessionId)) ->Property("SessionName", BehaviorValueProperty(&AzFramework::SessionConfig::m_sessionName)) ->Property("SessionProperties", BehaviorValueProperty(&AzFramework::SessionConfig::m_sessionProperties)) + ->Property("MatchmakingData", BehaviorValueProperty(&AzFramework::SessionConfig::m_matchmakingData)) ->Property("Status", BehaviorValueProperty(&AzFramework::SessionConfig::m_status)) ->Property("StatusReason", BehaviorValueProperty(&AzFramework::SessionConfig::m_statusReason)) ->Property("TerminationTime", BehaviorValueProperty(&AzFramework::SessionConfig::m_terminationTime)) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.cpp index 3d29fa2b71..5f0b6fd012 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.cpp @@ -105,6 +105,7 @@ namespace AWSGameLift session.m_status = AWSGameLiftSessionStatusNames[(int)gameSession.GetStatus()]; session.m_statusReason = AWSGameLiftSessionStatusReasons[(int)gameSession.GetStatusReason()]; session.m_terminationTime = gameSession.GetTerminationTime().Millis(); + session.m_matchmakingData = gameSession.GetMatchmakerData().c_str(); // TODO: Update the AWS Native SDK to get the new game session attributes. //session.m_dnsName = gameSession.GetDnsName(); diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftStartMatchmakingActivity.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftStartMatchmakingActivity.cpp index e535ea0c55..00a7773491 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftStartMatchmakingActivity.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftStartMatchmakingActivity.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -29,7 +30,7 @@ namespace AWSGameLift } Aws::Vector players; - for (const AWSGameLiftPlayerInformation& playerInfo : startMatchmakingRequest.m_players) + for (const AWSGameLiftPlayer& playerInfo : startMatchmakingRequest.m_players) { Aws::GameLift::Model::Player player; if (!playerInfo.m_playerId.empty()) @@ -109,7 +110,7 @@ namespace AWSGameLift if (isValid) { - for (const AWSGameLiftPlayerInformation& playerInfo : gameliftStartMatchmakingRequest->m_players) + for (const AWSGameLiftPlayer& playerInfo : gameliftStartMatchmakingRequest->m_players) { isValid &= !playerInfo.m_playerId.empty(); isValid &= AWSGameLiftActivityUtils::ValidatePlayerAttributes(playerInfo.m_playerAttributes); diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftStartMatchmakingRequest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftStartMatchmakingRequest.cpp index 03d802fd36..31e9c5eff7 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftStartMatchmakingRequest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftStartMatchmakingRequest.cpp @@ -14,53 +14,10 @@ namespace AWSGameLift { - void AWSGameLiftPlayerInformation::Reflect(AZ::ReflectContext* context) - { - if (auto serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(0) - ->Field("latencyInMs", &AWSGameLiftPlayerInformation::m_latencyInMs) - ->Field("playerAttributes", &AWSGameLiftPlayerInformation::m_playerAttributes) - ->Field("playerId", &AWSGameLiftPlayerInformation::m_playerId) - ->Field("team", &AWSGameLiftPlayerInformation::m_team); - - if (AZ::EditContext* editContext = serializeContext->GetEditContext()) - { - editContext->Class("AWSGameLiftPlayerInformation", "") - ->ClassElement(AZ::Edit::ClassElements::EditorData, "") - ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) - ->DataElement( - AZ::Edit::UIHandlers::Default, &AWSGameLiftPlayerInformation::m_latencyInMs, "LatencyInMs", - "A set of values, expressed in milliseconds, that indicates the amount of latency that" - "a player experiences when connected to AWS Regions") - ->DataElement( - AZ::Edit::UIHandlers::Default, &AWSGameLiftPlayerInformation::m_playerAttributes, "PlayerAttributes", - "A collection of key:value pairs containing player information for use in matchmaking") - ->DataElement( - AZ::Edit::UIHandlers::Default, &AWSGameLiftPlayerInformation::m_playerId, "PlayerId", - "A unique identifier for a player") - ->DataElement( - AZ::Edit::UIHandlers::Default, &AWSGameLiftPlayerInformation::m_team, "Team", - "Name of the team that the player is assigned to in a match"); - } - } - - if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) - { - behaviorContext->Class("AWSGameLiftPlayerInformation") - ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) - ->Property("LatencyInMs", BehaviorValueProperty(&AWSGameLiftPlayerInformation::m_latencyInMs)) - ->Property("PlayerAttributes", BehaviorValueProperty(&AWSGameLiftPlayerInformation::m_playerAttributes)) - ->Property("PlayerId", BehaviorValueProperty(&AWSGameLiftPlayerInformation::m_playerId)) - ->Property("Team", BehaviorValueProperty(&AWSGameLiftPlayerInformation::m_team)); - } - } - void AWSGameLiftStartMatchmakingRequest::Reflect(AZ::ReflectContext* context) { AzFramework::StartMatchmakingRequest::Reflect(context); - AWSGameLiftPlayerInformation::Reflect(context); + AWSGameLiftPlayer::Reflect(context); if (auto serializeContext = azrtti_cast(context)) { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp index 3b7c7ee827..1c3e8726fd 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp @@ -208,6 +208,7 @@ protected: sessionConfig.m_terminationTime = 0; sessionConfig.m_creatorId = "dummyCreatorId"; sessionConfig.m_sessionProperties["dummyKey"] = "dummyValue"; + sessionConfig.m_matchmakingData = "dummyMatchmakingData"; sessionConfig.m_sessionId = "dummyGameSessionId"; sessionConfig.m_sessionName = "dummyGameSessionName"; sessionConfig.m_ipAddress = "dummyIpAddress"; @@ -232,7 +233,7 @@ protected: request.m_configurationName = "dummyConfiguration"; request.m_ticketId = DummyMatchmakingTicketId; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"N\": \"1\"}"; player.m_playerId = DummyPlayerId; player.m_latencyInMs["us-east-1"] = 10; @@ -813,7 +814,7 @@ TEST_F(AWSGameLiftClientManagerTest, StartMatchmaking_CallWithInvalidRequest_Get { AWSGameLiftStartMatchmakingRequest request; request.m_configurationName = "dummyConfiguration"; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"A\": \"1\"}"; request.m_players.emplace_back(player); @@ -855,7 +856,7 @@ TEST_F(AWSGameLiftClientManagerTest, StartMatchmakingAsync_CallWithInvalidReques { AWSGameLiftStartMatchmakingRequest request; request.m_configurationName = "dummyConfiguration"; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"A\": \"1\"}"; request.m_players.emplace_back(player); diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftStartMatchmakingActivityTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftStartMatchmakingActivityTest.cpp index 3eed6b0448..6da932828c 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftStartMatchmakingActivityTest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftStartMatchmakingActivityTest.cpp @@ -8,6 +8,7 @@ #include #include +#include #include @@ -21,7 +22,7 @@ TEST_F(AWSGameLiftStartMatchmakingActivityTest, BuildAWSGameLiftStartMatchmaking request.m_configurationName = "dummyConfiguration"; request.m_ticketId = "dummyTicketId"; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"S\": \"test\"}"; player.m_playerId = "dummyPlayerId"; player.m_team = "dummyTeam"; @@ -58,7 +59,7 @@ TEST_F(AWSGameLiftStartMatchmakingActivityTest, ValidateStartMatchmakingRequest_ AWSGameLiftStartMatchmakingRequest request; request.m_ticketId = "dummyTicketId"; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"S\": \"test\"}"; player.m_playerId = "dummyPlayerId"; player.m_team = "dummyTeam"; @@ -89,7 +90,7 @@ TEST_F(AWSGameLiftStartMatchmakingActivityTest, ValidateStartMatchmakingRequest_ request.m_configurationName = "dummyConfiguration"; request.m_ticketId = "dummyTicketId"; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"S\": \"test\"}"; player.m_team = "dummyTeam"; player.m_latencyInMs["us-east-1"] = 10; @@ -107,7 +108,7 @@ TEST_F(AWSGameLiftStartMatchmakingActivityTest, ValidateStartMatchmakingRequest_ request.m_configurationName = "dummyConfiguration"; request.m_ticketId = "dummyTicketId"; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"A\": \"test\"}"; player.m_playerId = "dummyPlayerId"; player.m_team = "dummyTeam"; @@ -125,7 +126,7 @@ TEST_F(AWSGameLiftStartMatchmakingActivityTest, ValidateStartMatchmakingRequest_ AWSGameLiftStartMatchmakingRequest request; request.m_configurationName = "dummyConfiguration"; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"S\": \"test\"}"; player.m_playerId = "dummyPlayerId"; player.m_team = "dummyTeam"; @@ -142,7 +143,7 @@ TEST_F(AWSGameLiftStartMatchmakingActivityTest, ValidateStartMatchmakingRequest_ request.m_ticketId = "dummyTicketId"; request.m_configurationName = "dummyConfiguration"; - AWSGameLiftPlayerInformation player; + AWSGameLiftPlayer player; player.m_playerAttributes["dummy"] = "{\"S\": \"test\"}"; player.m_playerId = "dummyPlayerId"; player.m_team = "dummyTeam"; diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake index 0930c42370..cd54414cc1 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake @@ -7,6 +7,8 @@ # set(FILES + ../AWSGameLiftCommon/Include/AWSGameLiftPlayer.h + ../AWSGameLiftCommon/Source/AWSGameLiftPlayer.cpp ../AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h Include/Request/AWSGameLiftAcceptMatchRequest.h Include/Request/AWSGameLiftCreateSessionOnQueueRequest.h diff --git a/Gems/AWSGameLift/Code/AWSGameLiftCommon/Include/AWSGameLiftPlayer.h b/Gems/AWSGameLift/Code/AWSGameLiftCommon/Include/AWSGameLiftPlayer.h new file mode 100644 index 0000000000..1fbdadc8bf --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftCommon/Include/AWSGameLiftPlayer.h @@ -0,0 +1,44 @@ +/* + * 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 +#include +#include + +namespace AWSGameLift +{ + //! AWSGameLiftPlayer + //! Information on each player to be matched + //! This information must include a player ID, and may contain player attributes and latency data to be used in the matchmaking process + //! After a successful match, Player objects contain the name of the team the player is assigned to + struct AWSGameLiftPlayer + { + AZ_RTTI(AWSGameLiftPlayer, "{B62C118E-C55D-4903-8ECB-E58E8CA613C4}"); + static void Reflect(AZ::ReflectContext* context); + + AWSGameLiftPlayer() = default; + virtual ~AWSGameLiftPlayer() = default; + + // A map of region names to latencies in millseconds, that indicates + // the amount of latency that a player experiences when connected to AWS Regions + AZStd::unordered_map m_latencyInMs; + + // A collection of key:value pairs containing player information for use in matchmaking + // Player attribute keys must match the playerAttributes used in a matchmaking rule set + // Example: {"skill": "{\"N\": 23}", "gameMode": "{\"S\": \"deathmatch\"}"} + AZStd::unordered_map m_playerAttributes; + + // A unique identifier for a player + AZStd::string m_playerId; + + // Name of the team that the player is assigned to in a match + AZStd::string m_team; + }; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftCommon/Source/AWSGameLiftPlayer.cpp b/Gems/AWSGameLift/Code/AWSGameLiftCommon/Source/AWSGameLiftPlayer.cpp new file mode 100644 index 0000000000..80a3915a6b --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftCommon/Source/AWSGameLiftPlayer.cpp @@ -0,0 +1,58 @@ +/* + * 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 +#include + +#include + +namespace AWSGameLift +{ + void AWSGameLiftPlayer::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("latencyInMs", &AWSGameLiftPlayer::m_latencyInMs) + ->Field("playerAttributes", &AWSGameLiftPlayer::m_playerAttributes) + ->Field("playerId", &AWSGameLiftPlayer::m_playerId) + ->Field("team", &AWSGameLiftPlayer::m_team); + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("AWSGameLiftPlayer", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftPlayer::m_latencyInMs, "LatencyInMs", + "A set of values, expressed in milliseconds, that indicates the amount of latency that" + "a player experiences when connected to AWS Regions") + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftPlayer::m_playerAttributes, "PlayerAttributes", + "A collection of key:value pairs containing player information for use in matchmaking") + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftPlayer::m_playerId, "PlayerId", + "A unique identifier for a player") + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftPlayer::m_team, "Team", + "Name of the team that the player is assigned to in a match"); + } + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("AWSGameLiftPlayer") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + ->Property("LatencyInMs", BehaviorValueProperty(&AWSGameLiftPlayer::m_latencyInMs)) + ->Property("PlayerAttributes", BehaviorValueProperty(&AWSGameLiftPlayer::m_playerAttributes)) + ->Property("PlayerId", BehaviorValueProperty(&AWSGameLiftPlayer::m_playerId)) + ->Property("Team", BehaviorValueProperty(&AWSGameLiftPlayer::m_team)); + } + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/CMakeLists.txt b/Gems/AWSGameLift/Code/AWSGameLiftServer/CMakeLists.txt index 798c6a6a25..d05ec46b52 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/CMakeLists.txt +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/CMakeLists.txt @@ -17,6 +17,7 @@ ly_add_target( awsgamelift_server_files.cmake INCLUDE_DIRECTORIES PUBLIC + ../AWSGameLiftCommon/Include Include PRIVATE ../AWSGameLiftCommon/Source @@ -54,6 +55,8 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) awsgamelift_server_tests_files.cmake INCLUDE_DIRECTORIES PRIVATE + ../AWSGameLiftCommon/Include + ../AWSGameLiftCommon/Source Tests Source BUILD_DEPENDENCIES diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h index e5b14319a8..777086e633 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h @@ -9,9 +9,11 @@ #pragma once #include -#include +#include +#include #include -#include + +#include namespace AWSGameLift { @@ -26,8 +28,21 @@ namespace AWSGameLift virtual ~IAWSGameLiftServerRequests() = default; //! Notify GameLift that the server process is ready to host a game session. - //! @return Whether the ProcessReady notification is sent to GameLift. + //! @return True if the ProcessReady notification is sent to GameLift successfully, false otherwise virtual bool NotifyGameLiftProcessReady() = 0; + + //! Sends a request to find new players for open slots in a game session created with FlexMatch. + //! @param ticketId Unique identifier for match backfill request ticket + //! @param players A set of data representing all players who are currently in the game session, + //! if not provided, system will use lazy loaded game session data which is not guaranteed to + //! be accurate (no latency data either) + //! @return True if StartMatchBackfill succeeds, false otherwise + virtual bool StartMatchBackfill(const AZStd::string& ticketId, const AZStd::vector& players) = 0; + + //! Cancels an active match backfill request that was created with StartMatchBackfill + //! @param ticketId Unique identifier of the backfill request ticket to be canceled + //! @return True if StopMatchBackfill succeeds, false otherwise + virtual bool StopMatchBackfill(const AZStd::string& ticketId) = 0; }; // IAWSGameLiftServerRequests EBus wrapper for scripting diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.cpp index e739933ab2..70d0063b61 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.cpp @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include #include #include @@ -112,6 +115,7 @@ namespace AWSGameLift { propertiesOutput = propertiesOutput.substr(0, propertiesOutput.size() - 1); // Trim last comma to fit array format } + sessionConfig.m_matchmakingData = gameSession.GetMatchmakerData().c_str(); sessionConfig.m_sessionId = gameSession.GetGameSessionId().c_str(); sessionConfig.m_ipAddress = gameSession.GetIpAddress().c_str(); sessionConfig.m_maxPlayer = gameSession.GetMaximumPlayerSessionCount(); @@ -133,6 +137,276 @@ namespace AWSGameLift return sessionConfig; } + bool AWSGameLiftServerManager::BuildServerMatchBackfillPlayer( + const AWSGameLiftPlayer& player, Aws::GameLift::Server::Model::Player& outBackfillPlayer) + { + outBackfillPlayer.SetPlayerId(player.m_playerId.c_str()); + outBackfillPlayer.SetTeam(player.m_team.c_str()); + for (auto latencyPair : player.m_latencyInMs) + { + outBackfillPlayer.AddLatencyInMs(latencyPair.first.c_str(), latencyPair.second); + } + + for (auto attributePair : player.m_playerAttributes) + { + Aws::GameLift::Server::Model::AttributeValue playerAttribute; + rapidjson::Document attributeDocument; + rapidjson::ParseResult parseResult = attributeDocument.Parse(attributePair.second.c_str()); + // player attribute json content should always be a single member object + if (parseResult && attributeDocument.IsObject() && attributeDocument.MemberCount() == 1) + { + if ((attributeDocument.HasMember(AWSGameLiftMatchmakingPlayerAttributeSTypeName) || + attributeDocument.HasMember(AWSGameLiftMatchmakingPlayerAttributeSServerTypeName)) && + attributeDocument.MemberBegin()->value.IsString()) + { + playerAttribute = Aws::GameLift::Server::Model::AttributeValue( + attributeDocument.MemberBegin()->value.GetString()); + } + else if ((attributeDocument.HasMember(AWSGameLiftMatchmakingPlayerAttributeNTypeName) || + attributeDocument.HasMember(AWSGameLiftMatchmakingPlayerAttributeNServerTypeName)) && + attributeDocument.MemberBegin()->value.IsNumber()) + { + playerAttribute = Aws::GameLift::Server::Model::AttributeValue( + attributeDocument.MemberBegin()->value.GetDouble()); + } + else if ((attributeDocument.HasMember(AWSGameLiftMatchmakingPlayerAttributeSDMTypeName) || + attributeDocument.HasMember(AWSGameLiftMatchmakingPlayerAttributeSDMServerTypeName)) && + attributeDocument.MemberBegin()->value.IsObject()) + { + playerAttribute = Aws::GameLift::Server::Model::AttributeValue::ConstructStringDoubleMap(); + for (auto iter = attributeDocument.MemberBegin()->value.MemberBegin(); + iter != attributeDocument.MemberBegin()->value.MemberEnd(); iter++) + { + if (iter->name.IsString() && iter->value.IsNumber()) + { + playerAttribute.AddStringAndDouble(iter->name.GetString(), iter->value.GetDouble()); + } + else + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftMatchmakingPlayerAttributeInvalidErrorMessage, + player.m_playerId.c_str(), "String double map key must be string type and value must be number type"); + return false; + } + } + } + else if ((attributeDocument.HasMember(AWSGameLiftMatchmakingPlayerAttributeSLTypeName) || + attributeDocument.HasMember(AWSGameLiftMatchmakingPlayerAttributeSLServerTypeName)) && + attributeDocument.MemberBegin()->value.IsArray()) + { + playerAttribute = Aws::GameLift::Server::Model::AttributeValue::ConstructStringList(); + for (auto iter = attributeDocument.MemberBegin()->value.Begin(); + iter != attributeDocument.MemberBegin()->value.End(); iter++) + { + if (iter->IsString()) + { + playerAttribute.AddString(iter->GetString()); + } + else + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftMatchmakingPlayerAttributeInvalidErrorMessage, + player.m_playerId.c_str(), "String list element must be string type"); + return false; + } + } + } + else + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftMatchmakingPlayerAttributeInvalidErrorMessage, + player.m_playerId.c_str(), "S, N, SDM or SLM is expected as attribute type."); + return false; + } + } + else + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftMatchmakingPlayerAttributeInvalidErrorMessage, + player.m_playerId.c_str(), rapidjson::GetParseError_En(parseResult.Code())); + return false; + } + outBackfillPlayer.AddPlayerAttribute(attributePair.first.c_str(), playerAttribute); + } + return true; + } + + AZStd::vector AWSGameLiftServerManager::GetActiveServerMatchBackfillPlayers() + { + AZStd::vector activePlayers; + // Keep processing only when game session has matchmaking data + if (IsMatchmakingDataValid()) + { + auto activePlayerSessions = GetActivePlayerSessions(); + for (auto playerSession : activePlayerSessions) + { + AWSGameLiftPlayer player; + if (BuildActiveServerMatchBackfillPlayer(playerSession.GetPlayerId().c_str(), player)) + { + activePlayers.push_back(player); + } + } + } + return activePlayers; + } + + bool AWSGameLiftServerManager::IsMatchmakingDataValid() + { + return m_matchmakingData.IsObject() && + m_matchmakingData.HasMember(AWSGameLiftMatchmakingConfigurationKeyName) && + m_matchmakingData.HasMember(AWSGameLiftMatchmakingTeamsKeyName); + } + + AZStd::vector AWSGameLiftServerManager::GetActivePlayerSessions() + { + Aws::GameLift::Server::Model::DescribePlayerSessionsRequest describeRequest; + describeRequest.SetGameSessionId(m_gameSession.GetGameSessionId()); + describeRequest.SetPlayerSessionStatusFilter( + Aws::GameLift::Server::Model::PlayerSessionStatusMapper::GetNameForPlayerSessionStatus( + Aws::GameLift::Server::Model::PlayerSessionStatus::ACTIVE)); + int maxPlayerSession = m_gameSession.GetMaximumPlayerSessionCount(); + + AZStd::vector activePlayerSessions; + if (maxPlayerSession <= AWSGameLiftDescribePlayerSessionsPageSize) + { + describeRequest.SetLimit(maxPlayerSession); + auto outcome = m_gameLiftServerSDKWrapper->DescribePlayerSessions(describeRequest); + if (outcome.IsSuccess()) + { + for (auto playerSession : outcome.GetResult().GetPlayerSessions()) + { + activePlayerSessions.push_back(playerSession); + } + } + else + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftDescribePlayerSessionsErrorMessage, + outcome.GetError().GetErrorMessage().c_str()); + } + } + else + { + describeRequest.SetLimit(AWSGameLiftDescribePlayerSessionsPageSize); + while (true) + { + auto outcome = m_gameLiftServerSDKWrapper->DescribePlayerSessions(describeRequest); + if (outcome.IsSuccess()) + { + for (auto playerSession : outcome.GetResult().GetPlayerSessions()) + { + activePlayerSessions.push_back(playerSession); + } + if (outcome.GetResult().GetNextToken().empty()) + { + break; + } + else + { + describeRequest.SetNextToken(outcome.GetResult().GetNextToken()); + } + } + else + { + activePlayerSessions.clear(); + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftDescribePlayerSessionsErrorMessage, + outcome.GetError().GetErrorMessage().c_str()); + break; + } + } + } + return activePlayerSessions; + } + + bool AWSGameLiftServerManager::BuildActiveServerMatchBackfillPlayer(const AZStd::string& playerId, AWSGameLiftPlayer& outPlayer) + { + // As data is from GameLift service, assume it is always in correct format + rapidjson::Value& teams = m_matchmakingData[AWSGameLiftMatchmakingTeamsKeyName]; + + // Iterate through teams to find target player + for (rapidjson::SizeType teamIndex = 0; teamIndex < teams.Size(); ++teamIndex) + { + rapidjson::Value& players = teams[teamIndex][AWSGameLiftMatchmakingPlayersKeyName]; + + // Iterate through players under the team to find target player + for (rapidjson::SizeType playerIndex = 0; playerIndex < players.Size(); ++playerIndex) + { + if (std::strcmp(players[playerIndex][AWSGameLiftMatchmakingPlayerIdKeyName].GetString(), playerId.c_str()) == 0) + { + outPlayer.m_playerId = playerId; + outPlayer.m_team = teams[teamIndex][AWSGameLiftMatchmakingTeamNameKeyName].GetString(); + // Get player attributes if target player has + if (players[playerIndex].HasMember(AWSGameLiftMatchmakingPlayerAttributesKeyName)) + { + BuildServerMatchBackfillPlayerAttributes( + players[playerIndex][AWSGameLiftMatchmakingPlayerAttributesKeyName], outPlayer); + } + } + else + { + return false; + } + } + } + return true; + } + + void AWSGameLiftServerManager::BuildServerMatchBackfillPlayerAttributes( + const rapidjson::Value& playerAttributes, AWSGameLiftPlayer& outPlayer) + { + for (auto iter = playerAttributes.MemberBegin(); iter != playerAttributes.MemberEnd(); iter++) + { + AZStd::string attributeName = iter->name.GetString(); + + rapidjson::StringBuffer jsonStringBuffer; + rapidjson::Writer writer(jsonStringBuffer); + iter->value[AWSGameLiftMatchmakingPlayerAttributeValueKeyName].Accept(writer); + AZStd::string attributeType = iter->value[AWSGameLiftMatchmakingPlayerAttributeTypeKeyName].GetString(); + AZStd::string attributeValue = AZStd::string::format("{\"%s\": %s}", + attributeType.c_str(), jsonStringBuffer.GetString()); + + outPlayer.m_playerAttributes.emplace(attributeName, attributeValue); + } + } + + bool AWSGameLiftServerManager::BuildStartMatchBackfillRequest( + const AZStd::string& ticketId, + const AZStd::vector& players, + Aws::GameLift::Server::Model::StartMatchBackfillRequest& outRequest) + { + outRequest.SetGameSessionArn(m_gameSession.GetGameSessionId()); + outRequest.SetMatchmakingConfigurationArn(m_matchmakingData[AWSGameLiftMatchmakingConfigurationKeyName].GetString()); + if (!ticketId.empty()) + { + outRequest.SetTicketId(ticketId.c_str()); + } + + AZStd::vector requestPlayers(players); + if (players.size() == 0) + { + requestPlayers = GetActiveServerMatchBackfillPlayers(); + } + for (auto player : requestPlayers) + { + Aws::GameLift::Server::Model::Player backfillPlayer; + if (BuildServerMatchBackfillPlayer(player, backfillPlayer)) + { + outRequest.AddPlayer(backfillPlayer); + } + else + { + return false; + } + } + return true; + } + + void AWSGameLiftServerManager::BuildStopMatchBackfillRequest( + const AZStd::string& ticketId, Aws::GameLift::Server::Model::StopMatchBackfillRequest& outRequest) + { + outRequest.SetGameSessionArn(m_gameSession.GetGameSessionId()); + outRequest.SetMatchmakingConfigurationArn(m_matchmakingData[AWSGameLiftMatchmakingConfigurationKeyName].GetString()); + if (!ticketId.empty()) + { + outRequest.SetTicketId(ticketId.c_str()); + } + } + AZ::IO::Path AWSGameLiftServerManager::GetExternalSessionCertificate() { // TODO: Add support to get TLS cert file path @@ -238,7 +512,7 @@ namespace AWSGameLift Aws::GameLift::Server::ProcessParameters processReadyParameter = Aws::GameLift::Server::ProcessParameters( AZStd::bind(&AWSGameLiftServerManager::OnStartGameSession, this, AZStd::placeholders::_1), - AZStd::bind(&AWSGameLiftServerManager::OnUpdateGameSession, this), + AZStd::bind(&AWSGameLiftServerManager::OnUpdateGameSession, this, AZStd::placeholders::_1), AZStd::bind(&AWSGameLiftServerManager::OnProcessTerminate, this), AZStd::bind(&AWSGameLiftServerManager::OnHealthCheck, this), desc.m_port, Aws::GameLift::Server::LogParameters(logPaths)); @@ -260,6 +534,7 @@ namespace AWSGameLift void AWSGameLiftServerManager::OnStartGameSession(const Aws::GameLift::Server::Model::GameSession& gameSession) { + UpdateGameSessionData(gameSession); AzFramework::SessionConfig sessionConfig = BuildSessionConfig(gameSession); bool createSessionResult = true; @@ -311,10 +586,19 @@ namespace AWSGameLift return m_serverSDKInitialized && healthCheckResult; } - void AWSGameLiftServerManager::OnUpdateGameSession() + void AWSGameLiftServerManager::OnUpdateGameSession(const Aws::GameLift::Server::Model::UpdateGameSession& updateGameSession) { - // TODO: Perform game-specific tasks to prep for newly matched players - return; + Aws::GameLift::Server::Model::UpdateReason updateReason = updateGameSession.GetUpdateReason(); + if (updateReason == Aws::GameLift::Server::Model::UpdateReason::MATCHMAKING_DATA_UPDATED) + { + UpdateGameSessionData(updateGameSession.GetGameSession()); + } + AzFramework::SessionConfig sessionConfig = BuildSessionConfig(updateGameSession.GetGameSession()); + + AzFramework::SessionNotificationBus::Broadcast( + &AzFramework::SessionNotifications::OnUpdateSessionBegin, + sessionConfig, + Aws::GameLift::Server::Model::UpdateReasonMapper::GetNameForUpdateReason(updateReason).c_str()); } bool AWSGameLiftServerManager::RemoveConnectedPlayer(uint32_t playerConnectionId, AZStd::string& outPlayerSessionId) @@ -340,6 +624,92 @@ namespace AWSGameLift m_gameLiftServerSDKWrapper = AZStd::move(gameLiftServerSDKWrapper); } + bool AWSGameLiftServerManager::StartMatchBackfill(const AZStd::string& ticketId, const AZStd::vector& players) + { + if (!m_serverSDKInitialized) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerSDKNotInitErrorMessage); + return false; + } + + if (!IsMatchmakingDataValid()) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftMatchmakingDataMissingErrorMessage); + return false; + } + + Aws::GameLift::Server::Model::StartMatchBackfillRequest request; + if (!BuildStartMatchBackfillRequest(ticketId, players, request)) + { + return false; + } + + AZ_TracePrintf(AWSGameLiftServerManagerName, "Starting match backfill %s ...", ticketId.c_str()); + auto outcome = m_gameLiftServerSDKWrapper->StartMatchBackfill(request); + if (!outcome.IsSuccess()) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftStartMatchBackfillErrorMessage, + outcome.GetError().GetErrorMessage().c_str()); + return false; + } + else + { + AZ_TracePrintf(AWSGameLiftServerManagerName, "StartMatchBackfill request against Amazon GameLift service is complete."); + return true; + } + } + + bool AWSGameLiftServerManager::StopMatchBackfill(const AZStd::string& ticketId) + { + if (!m_serverSDKInitialized) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerSDKNotInitErrorMessage); + return false; + } + + if (!IsMatchmakingDataValid()) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftMatchmakingDataMissingErrorMessage); + return false; + } + + Aws::GameLift::Server::Model::StopMatchBackfillRequest request; + BuildStopMatchBackfillRequest(ticketId, request); + + AZ_TracePrintf(AWSGameLiftServerManagerName, "Stopping match backfill %s ...", ticketId.c_str()); + auto outcome = m_gameLiftServerSDKWrapper->StopMatchBackfill(request); + if (!outcome.IsSuccess()) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftStopMatchBackfillErrorMessage, + outcome.GetError().GetErrorMessage().c_str()); + return false; + } + else + { + AZ_TracePrintf(AWSGameLiftServerManagerName, "StopMatchBackfill request against Amazon GameLift service is complete."); + return true; + } + } + + void AWSGameLiftServerManager::UpdateGameSessionData(const Aws::GameLift::Server::Model::GameSession& gameSession) + { + AZ_TracePrintf(AWSGameLiftServerManagerName, "Lazy loading game session and matchmaking data from Amazon GameLift service ..."); + m_gameSession = Aws::GameLift::Server::Model::GameSession(gameSession); + if (m_gameSession.GetMatchmakerData().empty()) + { + m_matchmakingData.Parse("{}"); + } + else + { + rapidjson::ParseResult parseResult = m_matchmakingData.Parse(m_gameSession.GetMatchmakerData().c_str()); + if (!parseResult) + { + AZ_Error(AWSGameLiftServerManagerName, false, + AWSGameLiftMatchmakingDataInvalidErrorMessage, rapidjson::GetParseError_En(parseResult.Code())); + } + } + } + bool AWSGameLiftServerManager::ValidatePlayerJoinSession(const AzFramework::PlayerConnectionConfig& playerConnectionConfig) { uint32_t playerConnectionId = playerConnectionConfig.m_playerConnectionId; diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h index c0f1c00bea..fa2f783eca 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h @@ -11,11 +11,15 @@ #include #include +#include +#include #include #include #include #include #include + +#include #include namespace AWSGameLift @@ -66,6 +70,36 @@ namespace AWSGameLift "Invalid player connection config, player connection id: %d, player session id: %s"; static constexpr const char AWSGameLiftServerRemovePlayerSessionErrorMessage[] = "Failed to notify GameLift that the player with the player session id %s has disconnected from the server process. ErrorMessage: %s"; + static constexpr const char AWSGameLiftMatchmakingDataInvalidErrorMessage[] = + "Failed to parse GameLift matchmaking data. ErrorMessage: %s"; + static constexpr const char AWSGameLiftMatchmakingDataMissingErrorMessage[] = + "GameLift matchmaking data is missing or invalid to parse."; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeInvalidErrorMessage[] = + "Failed to build player %s attributes. ErrorMessage: %s"; + static constexpr const char AWSGameLiftDescribePlayerSessionsErrorMessage[] = + "Failed to describe player sessions. ErrorMessage: %s"; + static constexpr const char AWSGameLiftStartMatchBackfillErrorMessage[] = + "Failed to start match backfill. ErrorMessage: %s"; + static constexpr const char AWSGameLiftStopMatchBackfillErrorMessage[] = + "Failed to stop match backfill. ErrorMessage: %s"; + + static constexpr const char AWSGameLiftMatchmakingConfigurationKeyName[] = "matchmakingConfigurationArn"; + static constexpr const char AWSGameLiftMatchmakingTeamsKeyName[] = "teams"; + static constexpr const char AWSGameLiftMatchmakingTeamNameKeyName[] = "name"; + static constexpr const char AWSGameLiftMatchmakingPlayersKeyName[] = "players"; + static constexpr const char AWSGameLiftMatchmakingPlayerIdKeyName[] = "playerId"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributesKeyName[] = "attributes"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeTypeKeyName[] = "attributeType"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeValueKeyName[] = "valueAttribute"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSTypeName[] = "S"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSServerTypeName[] = "STRING"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeNTypeName[] = "N"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeNServerTypeName[] = "NUMBER"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSLTypeName[] = "SL"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSLServerTypeName[] = "STRING_LIST"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSDMTypeName[] = "SDM"; + static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSDMServerTypeName[] = "STRING_DOUBLE_MAP"; + static constexpr const uint16_t AWSGameLiftDescribePlayerSessionsPageSize = 30; AWSGameLiftServerManager(); virtual ~AWSGameLiftServerManager(); @@ -78,6 +112,8 @@ namespace AWSGameLift // AWSGameLiftServerRequestBus interface implementation bool NotifyGameLiftProcessReady() override; + bool StartMatchBackfill(const AZStd::string& ticketId, const AZStd::vector& players) override; + bool StopMatchBackfill(const AZStd::string& ticketId) override; // ISessionHandlingProviderRequests interface implementation void HandleDestroySession() override; @@ -92,18 +128,48 @@ namespace AWSGameLift //! Add connected player session id. bool AddConnectedPlayer(const AzFramework::PlayerConnectionConfig& playerConnectionConfig); + //! Get active server player data from lazy loaded game session for server match backfill + AZStd::vector GetActiveServerMatchBackfillPlayers(); + + //! Update local game session data to latest one + void UpdateGameSessionData(const Aws::GameLift::Server::Model::GameSession& gameSession); + private: //! Build the serverProcessDesc with appropriate server port number and log paths. GameLiftServerProcessDesc BuildGameLiftServerProcessDesc(); + //! Build active server player data from lazy loaded game session based on player id + bool BuildActiveServerMatchBackfillPlayer(const AZStd::string& playerId, AWSGameLiftPlayer& outPlayer); + + //! Build server player attribute data from lazy load matchmaking data + void BuildServerMatchBackfillPlayerAttributes(const rapidjson::Value& playerAttributes, AWSGameLiftPlayer& outPlayer); + + //! Build server player data for server match backfill + bool BuildServerMatchBackfillPlayer(const AWSGameLiftPlayer& player, Aws::GameLift::Server::Model::Player& outBackfillPlayer); + + //! Build start match backfill request for StartMatchBackfill operation + bool BuildStartMatchBackfillRequest( + const AZStd::string& ticketId, + const AZStd::vector& players, + Aws::GameLift::Server::Model::StartMatchBackfillRequest& outRequest); + + //! Build stop match backfill request for StopMatchBackfill operation + void BuildStopMatchBackfillRequest(const AZStd::string& ticketId, Aws::GameLift::Server::Model::StopMatchBackfillRequest& outRequest); + //! Build session config by using AWS GameLift Server GameSession Model. AzFramework::SessionConfig BuildSessionConfig(const Aws::GameLift::Server::Model::GameSession& gameSession); + //! Check whether matchmaking data is in proper format + bool IsMatchmakingDataValid(); + + //! Fetch active player sessions in game session. + AZStd::vector GetActivePlayerSessions(); + //! Callback function that the GameLift service invokes to activate a new game session. void OnStartGameSession(const Aws::GameLift::Server::Model::GameSession& gameSession); //! Callback function that the GameLift service invokes to pass an updated game session object to the server process. - void OnUpdateGameSession(); + void OnUpdateGameSession(const Aws::GameLift::Server::Model::UpdateGameSession& updateGameSession); //! Callback function that the server process or GameLift service invokes to force the server process to shut down. void OnProcessTerminate(); @@ -125,5 +191,12 @@ namespace AWSGameLift using PlayerConnectionId = uint32_t; using PlayerSessionId = AZStd::string; AZStd::unordered_map m_connectedPlayers; + + // Lazy loaded game session and matchmaking data + Aws::GameLift::Server::Model::GameSession m_gameSession; + // Matchmaking data contains a unique match ID, it identifies the matchmaker that created the match + // and describes the teams, team assignments, and players. + // Reference https://docs.aws.amazon.com/gamelift/latest/flexmatchguide/match-server.html#match-server-data + rapidjson::Document m_matchmakingData; }; } // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.cpp index 7e216f33be..d51c8eb8cb 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.cpp @@ -22,6 +22,12 @@ namespace AWSGameLift return Aws::GameLift::Server::ActivateGameSession(); } + Aws::GameLift::DescribePlayerSessionsOutcome GameLiftServerSDKWrapper::DescribePlayerSessions( + const Aws::GameLift::Server::Model::DescribePlayerSessionsRequest& describePlayerSessionsRequest) + { + return Aws::GameLift::Server::DescribePlayerSessions(describePlayerSessionsRequest); + } + Aws::GameLift::Server::InitSDKOutcome GameLiftServerSDKWrapper::InitSDK() { return Aws::GameLift::Server::InitSDK(); @@ -69,4 +75,17 @@ namespace AWSGameLift { return Aws::GameLift::Server::RemovePlayerSession(playerSessionId.c_str()); } + + Aws::GameLift::StartMatchBackfillOutcome GameLiftServerSDKWrapper::StartMatchBackfill( + const Aws::GameLift::Server::Model::StartMatchBackfillRequest& startMatchBackfillRequest) + { + return Aws::GameLift::Server::StartMatchBackfill(startMatchBackfillRequest); + } + + Aws::GameLift::GenericOutcome GameLiftServerSDKWrapper::StopMatchBackfill( + const Aws::GameLift::Server::Model::StopMatchBackfillRequest& stopMatchBackfillRequest) + { + return Aws::GameLift::Server::StopMatchBackfill(stopMatchBackfillRequest); + } + } // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.h index a656087206..e56366d75a 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.h @@ -33,6 +33,14 @@ namespace AWSGameLift //! @return Returns a generic outcome consisting of success or failure with an error message. virtual Aws::GameLift::GenericOutcome ActivateGameSession(); + //! Retrieves player session data, including settings, session metadata, and player data. + //! Use this action to get information for a single player session, + //! for all player sessions in a game session, or for all player sessions associated with a single player ID. + //! @param describePlayerSessionsRequest The request object describing which player sessions to retrieve. + //! @return If successful, returns a DescribePlayerSessionsOutcome object containing a set of player session objects that fit the request parameters. + virtual Aws::GameLift::DescribePlayerSessionsOutcome DescribePlayerSessions( + const Aws::GameLift::Server::Model::DescribePlayerSessionsRequest& describePlayerSessionsRequest); + //! Initializes the GameLift SDK. //! Should be called when the server starts, before any GameLift-dependent initialization happens. //! @return If successful, returns an InitSdkOutcome object indicating that the server process is ready to call ProcessReady(). @@ -56,5 +64,16 @@ namespace AWSGameLift //! @param playerSessionId Unique ID issued by the Amazon GameLift service in response to a call to the AWS SDK Amazon GameLift API action CreatePlayerSession. //! @return Returns a generic outcome consisting of success or failure with an error message. virtual Aws::GameLift::GenericOutcome RemovePlayerSession(const AZStd::string& playerSessionId); + + //! Sends a request to find new players for open slots in a game session created with FlexMatch. + //! When the match has been successfully, backfilled updated matchmaker data will be sent to the OnUpdateGameSession callback. + //! @param startMatchBackfillRequest This data type is used to send a matchmaking backfill request. + //! @return Returns a StartMatchBackfillOutcome object with the match backfill ticket or failure with an error message. + virtual Aws::GameLift::StartMatchBackfillOutcome StartMatchBackfill(const Aws::GameLift::Server::Model::StartMatchBackfillRequest& startMatchBackfillRequest); + + //! Cancels an active match backfill request that was created with StartMatchBackfill + //! @param stopMatchBackfillRequest This data type is used to cancel a matchmaking backfill request. + //! @return Returns a generic outcome consisting of success or failure with an error message. + virtual Aws::GameLift::GenericOutcome StopMatchBackfill(const Aws::GameLift::Server::Model::StopMatchBackfillRequest& stopMatchBackfillRequest); }; } // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerManagerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerManagerTest.cpp index 2c8929b297..d172734ec0 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerManagerTest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerManagerTest.cpp @@ -16,6 +16,136 @@ namespace UnitTest { + static constexpr const char TEST_SERVER_MATCHMAKING_DATA[] = +R"({ + "matchId":"testmatchid", + "matchmakingConfigurationArn":"testmatchconfig", + "teams":[ + {"name":"testteam", + "players":[ + {"playerId":"testplayer", + "attributes":{ + "skills":{ + "attributeType":"STRING_DOUBLE_MAP", + "valueAttribute":{"test1":10.0,"test2":20.0,"test3":30.0,"test4":40.0} + }, + "mode":{ + "attributeType":"STRING", + "valueAttribute":"testmode" + }, + "level":{ + "attributeType":"NUMBER", + "valueAttribute":10.0 + }, + "items":{ + "attributeType":"STRING_LIST", + "valueAttribute":["test1","test2","test3"] + } + }} + ]} + ] +})"; + + Aws::GameLift::Server::Model::StartMatchBackfillRequest GetTestStartMatchBackfillRequest() + { + Aws::GameLift::Server::Model::StartMatchBackfillRequest request; + request.SetMatchmakingConfigurationArn("testmatchconfig"); + Aws::GameLift::Server::Model::Player player; + player.SetPlayerId("testplayer"); + player.SetTeam("testteam"); + player.AddPlayerAttribute("mode", Aws::GameLift::Server::Model::AttributeValue("testmode")); + player.AddPlayerAttribute("level", Aws::GameLift::Server::Model::AttributeValue(10.0)); + auto sdmValue = Aws::GameLift::Server::Model::AttributeValue::ConstructStringDoubleMap(); + sdmValue.AddStringAndDouble("test1", 10.0); + player.AddPlayerAttribute("skills", sdmValue); + auto slValue = Aws::GameLift::Server::Model::AttributeValue::ConstructStringList(); + slValue.AddString("test1"); + player.AddPlayerAttribute("items", slValue); + player.AddLatencyInMs("testregion", 10); + request.AddPlayer(player); + request.SetTicketId("testticket"); + return request; + } + + AWSGameLiftPlayer GetTestGameLiftPlayer() + { + AWSGameLiftPlayer player; + player.m_team = "testteam"; + player.m_playerId = "testplayer"; + player.m_playerAttributes.emplace("mode", "{\"S\": \"testmode\"}"); + player.m_playerAttributes.emplace("level", "{\"N\": 10.0}"); + player.m_playerAttributes.emplace("skills", "{\"SDM\": {\"test1\":10.0}}"); + player.m_playerAttributes.emplace("items", "{\"SL\": [\"test1\"]}"); + player.m_latencyInMs.emplace("testregion", 10); + return player; + } + + MATCHER_P(StartMatchBackfillRequestMatcher, expectedRequest, "") + { + // Custome matcher for checking the SearchSessionsResponse type argument. + AZ_UNUSED(result_listener); + if (strcmp(arg.GetGameSessionArn().c_str(), expectedRequest.GetGameSessionArn().c_str()) != 0) + { + return false; + } + if (strcmp(arg.GetMatchmakingConfigurationArn().c_str(), expectedRequest.GetMatchmakingConfigurationArn().c_str()) != 0) + { + return false; + } + if (strcmp(arg.GetTicketId().c_str(), expectedRequest.GetTicketId().c_str()) != 0) + { + return false; + } + if (arg.GetPlayers().size() != expectedRequest.GetPlayers().size()) + { + return false; + } + for (int playerIndex = 0; playerIndex < expectedRequest.GetPlayers().size(); playerIndex++) + { + auto actualPlayerAttributes = arg.GetPlayers()[playerIndex].GetPlayerAttributes(); + auto expectedPlayerAttributes = expectedRequest.GetPlayers()[playerIndex].GetPlayerAttributes(); + if (actualPlayerAttributes.size() != expectedPlayerAttributes.size()) + { + return false; + } + for (auto attributePair : expectedPlayerAttributes) + { + if (actualPlayerAttributes.find(attributePair.first) == actualPlayerAttributes.end()) + { + return false; + } + if (!(attributePair.second.GetType() == actualPlayerAttributes[attributePair.first].GetType() && + (attributePair.second.GetS() == actualPlayerAttributes[attributePair.first].GetS() || + attributePair.second.GetN() == actualPlayerAttributes[attributePair.first].GetN() || + attributePair.second.GetSL() == actualPlayerAttributes[attributePair.first].GetSL() || + attributePair.second.GetSDM() == actualPlayerAttributes[attributePair.first].GetSDM()))) + { + return false; + } + } + + auto actualLatencies = arg.GetPlayers()[playerIndex].GetLatencyInMs(); + auto expectedLatencies = expectedRequest.GetPlayers()[playerIndex].GetLatencyInMs(); + if (actualLatencies.size() != expectedLatencies.size()) + { + return false; + } + for (auto latencyPair : expectedLatencies) + { + if (actualLatencies.find(latencyPair.first) == actualLatencies.end()) + { + return false; + } + if (latencyPair.second != actualLatencies[latencyPair.first]) + { + return false; + } + } + } + + return true; + } + class SessionNotificationsHandlerMock : public AzFramework::SessionNotificationBus::Handler { @@ -33,6 +163,7 @@ namespace UnitTest MOCK_METHOD0(OnSessionHealthCheck, bool()); MOCK_METHOD1(OnCreateSessionBegin, bool(const AzFramework::SessionConfig&)); MOCK_METHOD0(OnDestroySessionBegin, bool()); + MOCK_METHOD2(OnUpdateSessionBegin, void(const AzFramework::SessionConfig&, const AZStd::string&)); }; class GameLiftServerManagerTest @@ -228,6 +359,64 @@ namespace UnitTest AZ_TEST_STOP_TRACE_SUPPRESSION(1); } + TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithUnknownReason_OnUpdateSessionBeginGetCalledOnce) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnUpdateSessionBegin(testing::_, testing::_)).Times(1); + + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onUpdateGameSessionFunc( + Aws::GameLift::Server::Model::UpdateGameSession( + Aws::GameLift::Server::Model::GameSession(), + Aws::GameLift::Server::Model::UpdateReason::UNKNOWN, + "testticket")); + } + + TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithEmptyMatchmakingData_OnUpdateSessionBeginGetCalledOnce) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnUpdateSessionBegin(testing::_, testing::_)).Times(1); + + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onUpdateGameSessionFunc( + Aws::GameLift::Server::Model::UpdateGameSession( + Aws::GameLift::Server::Model::GameSession(), + Aws::GameLift::Server::Model::UpdateReason::MATCHMAKING_DATA_UPDATED, + "testticket")); + } + + TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithValidMatchmakingData_OnUpdateSessionBeginGetCalledOnce) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnUpdateSessionBegin(testing::_, testing::_)).Times(1); + + Aws::GameLift::Server::Model::GameSession gameSession; + gameSession.SetMatchmakerData(TEST_SERVER_MATCHMAKING_DATA); + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onUpdateGameSessionFunc( + Aws::GameLift::Server::Model::UpdateGameSession( + gameSession, Aws::GameLift::Server::Model::UpdateReason::MATCHMAKING_DATA_UPDATED, "testticket")); + } + + TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithInvalidMatchmakingData_OnUpdateSessionBeginGetCalledOnce) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnUpdateSessionBegin(testing::_, testing::_)).Times(1); + + Aws::GameLift::Server::Model::GameSession gameSession; + gameSession.SetMatchmakerData("{invalid}"); + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onUpdateGameSessionFunc( + Aws::GameLift::Server::Model::UpdateGameSession( + gameSession, Aws::GameLift::Server::Model::UpdateReason::MATCHMAKING_DATA_UPDATED, "testticket")); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + TEST_F(GameLiftServerManagerTest, ValidatePlayerJoinSession_CallWithInvalidConnectionConfig_GetFalseResultAndExpectedErrorLog) { AZ_TEST_START_TRACE_SUPPRESSION; @@ -425,4 +614,331 @@ namespace UnitTest } AZ_TEST_STOP_TRACE_SUPPRESSION(testThreadNumber - 1); // The player is only disconnected once. } + + TEST_F(GameLiftServerManagerTest, UpdateGameSessionData_CallWithInvalidMatchmakingData_GetExpectedError) + { + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->SetupTestMatchmakingData("{invalid}"); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, GetActiveServerMatchBackfillPlayers_CallWithInvalidMatchmakingData_GetEmptyResult) + { + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->SetupTestMatchmakingData("{invalid}"); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + + auto actualResult = m_serverManager->GetTestServerMatchBackfillPlayers(); + EXPECT_TRUE(actualResult.empty()); + } + + TEST_F(GameLiftServerManagerTest, GetActiveServerMatchBackfillPlayers_CallWithEmptyMatchmakingData_GetEmptyResult) + { + m_serverManager->SetupTestMatchmakingData(""); + + auto actualResult = m_serverManager->GetTestServerMatchBackfillPlayers(); + EXPECT_TRUE(actualResult.empty()); + } + + TEST_F(GameLiftServerManagerTest, GetActiveServerMatchBackfillPlayers_CallButDescribePlayerError_GetEmptyResult) + { + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + Aws::GameLift::GameLiftError error; + Aws::GameLift::DescribePlayerSessionsOutcome errorOutcome(error); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), DescribePlayerSessions(testing::_)) + .Times(1) + .WillOnce(Return(errorOutcome)); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->GetTestServerMatchBackfillPlayers(); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_TRUE(actualResult.empty()); + } + + TEST_F(GameLiftServerManagerTest, GetActiveServerMatchBackfillPlayers_CallButNoActivePlayer_GetEmptyResult) + { + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + Aws::GameLift::Server::Model::DescribePlayerSessionsResult result; + Aws::GameLift::DescribePlayerSessionsOutcome successOutcome(result); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), DescribePlayerSessions(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + + auto actualResult = m_serverManager->GetTestServerMatchBackfillPlayers(); + EXPECT_TRUE(actualResult.empty()); + } + + TEST_F(GameLiftServerManagerTest, GetActiveServerMatchBackfillPlayers_CallWithValidMatchmakingData_GetExpectedResult) + { + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + Aws::GameLift::Server::Model::PlayerSession playerSession; + playerSession.SetPlayerId("testplayer"); + Aws::GameLift::Server::Model::DescribePlayerSessionsResult result; + result.AddPlayerSessions(playerSession); + Aws::GameLift::DescribePlayerSessionsOutcome successOutcome(result); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), DescribePlayerSessions(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + + auto actualResult = m_serverManager->GetTestServerMatchBackfillPlayers(); + EXPECT_TRUE(actualResult.size() == 1); + EXPECT_TRUE(actualResult[0].m_team == "testteam"); + EXPECT_TRUE(actualResult[0].m_playerId == "testplayer"); + EXPECT_TRUE(actualResult[0].m_playerAttributes.size() == 4); + } + + TEST_F(GameLiftServerManagerTest, GetActiveServerMatchBackfillPlayers_CallWithMultiDescribePlayerButError_GetEmptyResult) + { + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA, 50); + + Aws::GameLift::GameLiftError error; + Aws::GameLift::DescribePlayerSessionsOutcome errorOutcome(error); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), DescribePlayerSessions(testing::_)) + .Times(1) + .WillOnce(Return(errorOutcome)); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->GetTestServerMatchBackfillPlayers(); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_TRUE(actualResult.empty()); + } + + TEST_F(GameLiftServerManagerTest, GetActiveServerMatchBackfillPlayers_CallWithMultiDescribePlayer_GetExpectedResult) + { + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA, 50); + + Aws::GameLift::Server::Model::PlayerSession playerSession1; + playerSession1.SetPlayerId("testplayer"); + Aws::GameLift::Server::Model::DescribePlayerSessionsResult result1; + result1.AddPlayerSessions(playerSession1); + result1.SetNextToken("testtoken"); + Aws::GameLift::DescribePlayerSessionsOutcome successOutcome1(result1); + + Aws::GameLift::Server::Model::PlayerSession playerSession2; + playerSession2.SetPlayerId("playernotinmatch"); + Aws::GameLift::Server::Model::DescribePlayerSessionsResult result2; + result2.AddPlayerSessions(playerSession2); + Aws::GameLift::DescribePlayerSessionsOutcome successOutcome2(result2); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), DescribePlayerSessions(testing::_)) + .WillOnce(Return(successOutcome1)) + .WillOnce(Return(successOutcome2)); + + auto actualResult = m_serverManager->GetTestServerMatchBackfillPlayers(); + EXPECT_TRUE(actualResult.size() == 1); + EXPECT_TRUE(actualResult[0].m_team == "testteam"); + EXPECT_TRUE(actualResult[0].m_playerId == "testplayer"); + EXPECT_TRUE(actualResult[0].m_playerAttributes.size() == 4); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_SDKNotInitialized_GetExpectedError) + { + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StartMatchBackfill("testticket", {}); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallWithEmptyMatchmakingData_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(""); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StartMatchBackfill("testticket", {}); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallWithInvalidPlayerAttribute_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + AWSGameLiftPlayer testPlayer = GetTestGameLiftPlayer(); + testPlayer.m_playerAttributes.clear(); + testPlayer.m_playerAttributes.emplace("invalidattribute", "{invalid}"); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StartMatchBackfill("testticket", { testPlayer }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallWithWrongPlayerAttributeType_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + AWSGameLiftPlayer testPlayer = GetTestGameLiftPlayer(); + testPlayer.m_playerAttributes.clear(); + testPlayer.m_playerAttributes.emplace("invalidattribute", "{\"SDM\": [\"test1\"]}"); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StartMatchBackfill("testticket", { testPlayer }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallWithUnexpectedPlayerAttributeType_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + AWSGameLiftPlayer testPlayer = GetTestGameLiftPlayer(); + testPlayer.m_playerAttributes.clear(); + testPlayer.m_playerAttributes.emplace("invalidattribute", "{\"UNEXPECTED\": [\"test1\"]}"); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StartMatchBackfill("testticket", { testPlayer }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallWithWrongSLPlayerAttributeValue_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + AWSGameLiftPlayer testPlayer = GetTestGameLiftPlayer(); + testPlayer.m_playerAttributes.clear(); + testPlayer.m_playerAttributes.emplace("invalidattribute", "{\"SL\": [10.0]}"); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StartMatchBackfill("testticket", { testPlayer }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallWithWrongSDMPlayerAttributeValue_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + AWSGameLiftPlayer testPlayer = GetTestGameLiftPlayer(); + testPlayer.m_playerAttributes.clear(); + testPlayer.m_playerAttributes.emplace("invalidattribute", "{\"SDM\": {10.0: \"test1\"}}"); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StartMatchBackfill("testticket", { testPlayer }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallWithValidPlayersData_GetExpectedResult) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + Aws::GameLift::Server::Model::StartMatchBackfillResult backfillResult; + Aws::GameLift::StartMatchBackfillOutcome backfillSuccessOutcome(backfillResult); + Aws::GameLift::Server::Model::StartMatchBackfillRequest request = GetTestStartMatchBackfillRequest(); + + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), StartMatchBackfill(StartMatchBackfillRequestMatcher(request))) + .Times(1) + .WillOnce(Return(backfillSuccessOutcome)); + + AWSGameLiftPlayer testPlayer = GetTestGameLiftPlayer(); + auto actualResult = m_serverManager->StartMatchBackfill("testticket", {testPlayer}); + EXPECT_TRUE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallWithoutGivingPlayersData_GetExpectedResult) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + Aws::GameLift::Server::Model::PlayerSession playerSession; + playerSession.SetPlayerId("testplayer"); + Aws::GameLift::Server::Model::DescribePlayerSessionsResult result; + result.AddPlayerSessions(playerSession); + Aws::GameLift::DescribePlayerSessionsOutcome successOutcome(result); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), DescribePlayerSessions(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + + Aws::GameLift::Server::Model::StartMatchBackfillResult backfillResult; + Aws::GameLift::StartMatchBackfillOutcome backfillSuccessOutcome(backfillResult); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), StartMatchBackfill(testing::_)) + .Times(1) + .WillOnce(Return(backfillSuccessOutcome)); + + auto actualResult = m_serverManager->StartMatchBackfill("testticket", {}); + EXPECT_TRUE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StartMatchBackfill_CallButStartBackfillFail_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + Aws::GameLift::Server::Model::PlayerSession playerSession; + playerSession.SetPlayerId("testplayer"); + Aws::GameLift::Server::Model::DescribePlayerSessionsResult result; + result.AddPlayerSessions(playerSession); + Aws::GameLift::DescribePlayerSessionsOutcome successOutcome(result); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), DescribePlayerSessions(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + + Aws::GameLift::GameLiftError error; + Aws::GameLift::StartMatchBackfillOutcome errorOutcome(error); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), StartMatchBackfill(testing::_)) + .Times(1) + .WillOnce(Return(errorOutcome)); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StartMatchBackfill("testticket", {}); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StopMatchBackfill_SDKNotInitialized_GetExpectedError) + { + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StopMatchBackfill("testticket"); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StopMatchBackfill_CallWithEmptyMatchmakingData_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(""); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StopMatchBackfill("testticket"); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StopMatchBackfill_CallAndSuccessOutcome_GetExpectedResult) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), StopMatchBackfill(testing::_)) + .Times(1) + .WillOnce(Return(Aws::GameLift::GenericOutcome(nullptr))); + + auto actualResult = m_serverManager->StopMatchBackfill("testticket"); + EXPECT_TRUE(actualResult); + } + + TEST_F(GameLiftServerManagerTest, StopMatchBackfill_CallButErrorOutcome_GetExpectedError) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->SetupTestMatchmakingData(TEST_SERVER_MATCHMAKING_DATA); + + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), StopMatchBackfill(testing::_)) + .Times(1) + .WillOnce(Return(Aws::GameLift::GenericOutcome())); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto actualResult = m_serverManager->StopMatchBackfill("testticket"); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(actualResult); + } } // namespace UnitTest diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerMocks.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerMocks.h index 24336680b0..7ab9c51cd1 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerMocks.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerMocks.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -40,17 +41,25 @@ namespace UnitTest MOCK_METHOD1(AcceptPlayerSession, GenericOutcome(const std::string&)); MOCK_METHOD0(ActivateGameSession, GenericOutcome()); + MOCK_METHOD1(DescribePlayerSessions, DescribePlayerSessionsOutcome( + const Aws::GameLift::Server::Model::DescribePlayerSessionsRequest&)); MOCK_METHOD0(InitSDK, Server::InitSDKOutcome()); MOCK_METHOD1(ProcessReady, GenericOutcome(const Server::ProcessParameters& processParameters)); MOCK_METHOD0(ProcessEnding, GenericOutcome()); MOCK_METHOD1(RemovePlayerSession, GenericOutcome(const AZStd::string& playerSessionId)); MOCK_METHOD0(GetTerminationTime, AZStd::string()); + MOCK_METHOD1(StartMatchBackfill, StartMatchBackfillOutcome( + const Aws::GameLift::Server::Model::StartMatchBackfillRequest&)); + MOCK_METHOD1(StopMatchBackfill, GenericOutcome( + const Aws::GameLift::Server::Model::StopMatchBackfillRequest&)); + GenericOutcome ProcessReadyMock(const Server::ProcessParameters& processParameters) { m_healthCheckFunc = processParameters.getOnHealthCheck(); m_onStartGameSessionFunc = processParameters.getOnStartGameSession(); m_onProcessTerminateFunc = processParameters.getOnProcessTerminate(); + m_onUpdateGameSessionFunc = processParameters.getOnUpdateGameSession(); GenericOutcome successOutcome(nullptr); return successOutcome; @@ -59,6 +68,7 @@ namespace UnitTest AZStd::function m_healthCheckFunc; AZStd::function m_onProcessTerminateFunc; AZStd::function m_onStartGameSessionFunc; + AZStd::function m_onUpdateGameSessionFunc; }; class AWSGameLiftServerManagerMock @@ -78,12 +88,25 @@ namespace UnitTest m_gameLiftServerSDKWrapperMockPtr = nullptr; } + void SetupTestMatchmakingData(const AZStd::string& matchmakingData, int maxPlayer = 10) + { + m_testGameSession.SetMatchmakerData(matchmakingData.c_str()); + m_testGameSession.SetMaximumPlayerSessionCount(maxPlayer); + UpdateGameSessionData(m_testGameSession); + } + bool AddConnectedTestPlayer(const AzFramework::PlayerConnectionConfig& playerConnectionConfig) { return AddConnectedPlayer(playerConnectionConfig); } + AZStd::vector GetTestServerMatchBackfillPlayers() + { + return GetActiveServerMatchBackfillPlayers(); + } + NiceMock* m_gameLiftServerSDKWrapperMockPtr; + Aws::GameLift::Server::Model::GameSession m_testGameSession; }; class AWSGameLiftServerSystemComponentMock diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake index 95b5e1d2d2..9039c9943e 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake @@ -7,6 +7,8 @@ # set(FILES + ../AWSGameLiftCommon/Include/AWSGameLiftPlayer.h + ../AWSGameLiftCommon/Source/AWSGameLiftPlayer.cpp ../AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h Include/Request/IAWSGameLiftServerRequests.h Source/AWSGameLiftServerManager.cpp diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl index 05059aba6f..523353f8fa 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl @@ -9,7 +9,7 @@ #include "Skin_Common.azsli" // SRGs -#include +#include #include // Pass Output diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype index 0969cc36e8..e4da9c6022 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype @@ -973,15 +973,15 @@ "tag": "ForwardPass" }, { - "file": "Shaders/Shadow/Shadowmap.shader", + "file": "Shaders/Shadow/ShadowmapSkin.shader", "tag": "Shadowmap" }, { - "file": "Shaders/Depth/DepthPass.shader", + "file": "Shaders/Depth/DepthPassSkin.shader", "tag": "DepthPass" }, { - "file": "Shaders/MotionVector/MeshMotionVector.shader", + "file": "Shaders/MotionVector/MeshMotionVectorSkin.shader", "tag": "MeshMotionVector" } ], diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli index 7a2d2ee428..10526597cb 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli @@ -27,16 +27,6 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject return SceneSrg::GetObjectToWorldInverseTransposeMatrix(m_objectId); } - //[GFX TODO][ATOM-15280] Move wrinkle mask data from the default object srg into something specific to the Skin shader - uint m_wrinkle_mask_count; - float4 m_wrinkle_mask_weights[4]; - Texture2D m_wrinkle_masks[16]; - - float GetWrinkleMaskWeight(uint index) - { - return m_wrinkle_mask_weights[index / 4][index % 4]; - } - //! Reflection Probe (smallest probe volume that overlaps the object position) struct ReflectionProbeData { diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Skin/SkinObjectSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Skin/SkinObjectSrg.azsli new file mode 100644 index 0000000000..d0766c295d --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Skin/SkinObjectSrg.azsli @@ -0,0 +1,81 @@ +/* + * 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 + +ShaderResourceGroup ObjectSrg : SRG_PerObject +{ + uint m_objectId; + + //! Returns the matrix for transforming points from Object Space to World Space. + float4x4 GetWorldMatrix() + { + return SceneSrg::GetObjectToWorldMatrix(m_objectId); + } + + //! Returns the inverse-transpose of the world matrix. + //! Commonly used to transform normals while supporting non-uniform scale. + float3x3 GetWorldMatrixInverseTranspose() + { + return SceneSrg::GetObjectToWorldInverseTransposeMatrix(m_objectId); + } + + uint m_wrinkle_mask_count; + float4 m_wrinkle_mask_weights[4]; + Texture2D m_wrinkle_masks[16]; + + float GetWrinkleMaskWeight(uint index) + { + return m_wrinkle_mask_weights[index / 4][index % 4]; + } + + //! Reflection Probe (smallest probe volume that overlaps the object position) + struct ReflectionProbeData + { + row_major float3x4 m_modelToWorld; + row_major float3x4 m_modelToWorldInverse; // does not include extents + float3 m_outerObbHalfLengths; + float3 m_innerObbHalfLengths; + float m_padding; + bool m_useReflectionProbe; + bool m_useParallaxCorrection; + }; + + ReflectionProbeData m_reflectionProbeData; + TextureCube m_reflectionProbeCubeMap; + + float4x4 GetReflectionProbeWorldMatrix() + { + float4x4 modelToWorld = float4x4( + float4(1, 0, 0, 0), + float4(0, 1, 0, 0), + float4(0, 0, 1, 0), + float4(0, 0, 0, 1)); + + modelToWorld[0] = m_reflectionProbeData.m_modelToWorld[0]; + modelToWorld[1] = m_reflectionProbeData.m_modelToWorld[1]; + modelToWorld[2] = m_reflectionProbeData.m_modelToWorld[2]; + return modelToWorld; + } + + float4x4 GetReflectionProbeWorldMatrixInverse() + { + float4x4 modelToWorldInverse = float4x4( + float4(1, 0, 0, 0), + float4(0, 1, 0, 0), + float4(0, 0, 1, 0), + float4(0, 0, 0, 1)); + + modelToWorldInverse[0] = m_reflectionProbeData.m_modelToWorldInverse[0]; + modelToWorldInverse[1] = m_reflectionProbeData.m_modelToWorldInverse[1]; + modelToWorldInverse[2] = m_reflectionProbeData.m_modelToWorldInverse[2]; + return modelToWorldInverse; + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPass.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPass.azsl index 8eb657f8f5..876f4b1a61 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPass.azsl @@ -6,30 +6,7 @@ * */ -#include #include +#include -struct VSInput -{ - float3 m_position : POSITION; -}; - -struct VSDepthOutput -{ - float4 m_position : SV_Position; -}; - -VSDepthOutput DepthPassVS(VSInput IN) -{ - VSDepthOutput OUT; - - float4x4 objectToWorld = ObjectSrg::GetWorldMatrix(); - float4 worldPosition = mul(objectToWorld, float4(IN.m_position, 1.0)); - OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, worldPosition); - - return OUT; -} - - - - +// Use the depth pass shader with the default object srg diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassCommon.azsli b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassCommon.azsli new file mode 100644 index 0000000000..f658dd13da --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassCommon.azsli @@ -0,0 +1,36 @@ +/* + * 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 + +struct VSInput +{ + float3 m_position : POSITION; +}; + +struct VSDepthOutput +{ + float4 m_position : SV_Position; +}; + +VSDepthOutput DepthPassVS(VSInput IN) +{ + VSDepthOutput OUT; + + float4x4 objectToWorld = ObjectSrg::GetWorldMatrix(); + float4 worldPosition = mul(objectToWorld, float4(IN.m_position, 1.0)); + OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, worldPosition); + + return OUT; +} + + + + diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.azsl new file mode 100644 index 0000000000..0947be6cf8 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.azsl @@ -0,0 +1,12 @@ +/* + * 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 +#include + +// Use the depth pass shader with the skin object srg diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.shader new file mode 100644 index 0000000000..de6c989223 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.shader @@ -0,0 +1,24 @@ +{ + "Source" : "DepthPassSkin", + + "DepthStencilState" : { + "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } + }, + + "CompilerHints" : { + "DisableOptimizations" : false + }, + + "ProgramSettings" : + { + "EntryPoints": + [ + { + "name": "DepthPassVS", + "type" : "Vertex" + } + ] + }, + + "DrawList" : "depth" +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.azsl index 11cf20dcaf..a14d07194f 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.azsl @@ -6,77 +6,7 @@ * */ -#include -#include - #include -#include +#include -struct VSInput -{ - float3 m_position : POSITION; - - // This gets set automatically by the system at runtime only if it's available. - // There is a soft naming convention that associates this with o_prevPosition_isBound, which will be set to true whenever m_optional_prevPosition is available. - // (search "m_optional_" in ShaderVariantAssetBuilder for details on the naming convention). - // [GFX TODO][ATOM-14475]: Come up with a more elegant way to associate the isBound flag with the input stream. - // Vertex position of last frame to capture small scale motion due to vertex animation - float3 m_optional_prevPosition : POSITIONT; -}; - -struct VSOutput -{ - float4 m_position : SV_Position; - float3 m_worldPos : TEXCOORD0; - float3 m_worldPosPrev: TEXCOORD1; -}; - -struct PSOutput -{ - float2 m_motion : SV_Target0; -}; - -// Indicates whether the vertex input struct's "m_optional_prevPosition" is bound. If false, it is not safe to read from m_optional_prevPosition. -// This option gets set automatically by the system at runtime; there is a soft naming convention that associates it with m_optional_prevPosition. -// (search "m_optional_" in ShaderVariantAssetBuilder for details on the naming convention). -// [GFX TODO][ATOM-14475]: Come up with a more elegant way to associate the isBound flag with the input stream. -option bool o_prevPosition_isBound; - -VSOutput MainVS(VSInput IN) -{ - VSOutput OUT; - - OUT.m_worldPos = mul(SceneSrg::GetObjectToWorldMatrix(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz; - OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(OUT.m_worldPos, 1.0)); - - if (o_prevPosition_isBound) - { - OUT.m_worldPosPrev = mul(SceneSrg::GetObjectToWorldMatrixPrev(ObjectSrg::m_objectId), float4(IN.m_optional_prevPosition, 1.0)).xyz; - } - else - { - OUT.m_worldPosPrev = mul(SceneSrg::GetObjectToWorldMatrixPrev(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz; - } - - return OUT; -} - -PSOutput MainPS(VSOutput IN) -{ - PSOutput OUT; - - // Current clip position - float4 clipPos = mul(ViewSrg::m_viewProjectionMatrix, float4(IN.m_worldPos, 1.0)); - - // Reprojected last frame's clip position, for skinned mesh it also implies last key frame - float4 clipPosPrev = mul(ViewSrg::m_viewProjectionPrevMatrix, float4(IN.m_worldPosPrev, 1.0)); - - float2 motion = (clipPos.xy / clipPos.w - clipPosPrev.xy / clipPosPrev.w) * 0.5; - - OUT.m_motion = motion; - - // Flip y to line up with uv coordinates - OUT.m_motion.y = -OUT.m_motion.y; - - return OUT; -} +// Use the mesh motion vector with the default object srg diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorCommon.azsli b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorCommon.azsli new file mode 100644 index 0000000000..c934b056db --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorCommon.azsli @@ -0,0 +1,83 @@ +/* + * 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 +#include + +#include + +struct VSInput +{ + float3 m_position : POSITION; + + // This gets set automatically by the system at runtime only if it's available. + // There is a soft naming convention that associates this with o_prevPosition_isBound, which will be set to true whenever m_optional_prevPosition is available. + // (search "m_optional_" in ShaderVariantAssetBuilder for details on the naming convention). + // [GFX TODO][ATOM-14475]: Come up with a more elegant way to associate the isBound flag with the input stream. + // Vertex position of last frame to capture small scale motion due to vertex animation + float3 m_optional_prevPosition : POSITIONT; +}; + +struct VSOutput +{ + float4 m_position : SV_Position; + float3 m_worldPos : TEXCOORD0; + float3 m_worldPosPrev: TEXCOORD1; +}; + +struct PSOutput +{ + float2 m_motion : SV_Target0; +}; + +// Indicates whether the vertex input struct's "m_optional_prevPosition" is bound. If false, it is not safe to read from m_optional_prevPosition. +// This option gets set automatically by the system at runtime; there is a soft naming convention that associates it with m_optional_prevPosition. +// (search "m_optional_" in ShaderVariantAssetBuilder for details on the naming convention). +// [GFX TODO][ATOM-14475]: Come up with a more elegant way to associate the isBound flag with the input stream. +option bool o_prevPosition_isBound; + +VSOutput MainVS(VSInput IN) +{ + VSOutput OUT; + + OUT.m_worldPos = mul(SceneSrg::GetObjectToWorldMatrix(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz; + OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(OUT.m_worldPos, 1.0)); + + if (o_prevPosition_isBound) + { + OUT.m_worldPosPrev = mul(SceneSrg::GetObjectToWorldMatrixPrev(ObjectSrg::m_objectId), float4(IN.m_optional_prevPosition, 1.0)).xyz; + } + else + { + OUT.m_worldPosPrev = mul(SceneSrg::GetObjectToWorldMatrixPrev(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz; + } + + return OUT; +} + +PSOutput MainPS(VSOutput IN) +{ + PSOutput OUT; + + // Current clip position + float4 clipPos = mul(ViewSrg::m_viewProjectionMatrix, float4(IN.m_worldPos, 1.0)); + + // Reprojected last frame's clip position, for skinned mesh it also implies last key frame + float4 clipPosPrev = mul(ViewSrg::m_viewProjectionPrevMatrix, float4(IN.m_worldPosPrev, 1.0)); + + float2 motion = (clipPos.xy / clipPos.w - clipPosPrev.xy / clipPosPrev.w) * 0.5; + + OUT.m_motion = motion; + + // Flip y to line up with uv coordinates + OUT.m_motion.y = -OUT.m_motion.y; + + return OUT; +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.azsl new file mode 100644 index 0000000000..fc089ac7a3 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.azsl @@ -0,0 +1,12 @@ +/* + * 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 +#include + +// Use the mesh motion vector with the skin object srg diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.shader b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.shader new file mode 100644 index 0000000000..9a50e4e2cf --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.shader @@ -0,0 +1,24 @@ +{ + "Source" : "MeshMotionVectorSkin", + + "DepthStencilState" : { + "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } + }, + + "DrawList" : "motion", + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/Shadowmap.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/Shadowmap.azsl index 713ff7c393..7a958a73ca 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/Shadowmap.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/Shadowmap.azsl @@ -6,27 +6,7 @@ * */ -#include -#include #include +#include -struct VertexInput -{ - float3 m_position : POSITION; -}; - -struct VertexOutput -{ - float4 m_position : SV_Position; -}; - -VertexOutput MainVS(VertexInput input) -{ - const float4x4 worldMatrix = ObjectSrg::GetWorldMatrix(); - VertexOutput output; - - const float3 worldPosition = mul(worldMatrix, float4(input.m_position, 1.0)).xyz; - output.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(worldPosition, 1.0)); - - return output; -} +// Use the shadowmap shader with the default object srg diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapCommon.azsli b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapCommon.azsli new file mode 100644 index 0000000000..213fb18dc4 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapCommon.azsli @@ -0,0 +1,33 @@ +/* + * 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 +#include + +struct VertexInput +{ + float3 m_position : POSITION; +}; + +struct VertexOutput +{ + float4 m_position : SV_Position; +}; + +VertexOutput MainVS(VertexInput input) +{ + const float4x4 worldMatrix = ObjectSrg::GetWorldMatrix(); + VertexOutput output; + + const float3 worldPosition = mul(worldMatrix, float4(input.m_position, 1.0)).xyz; + output.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(worldPosition, 1.0)); + + return output; +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.azsl new file mode 100644 index 0000000000..6f0d8e1a31 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.azsl @@ -0,0 +1,12 @@ +/* + * 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 +#include + +// Use the shadowmap shader with the skin object srg diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.shader new file mode 100644 index 0000000000..14c1352c08 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.shader @@ -0,0 +1,26 @@ +{ + "Source" : "ShadowmapSkin", + + "DepthStencilState" : { + "Depth" : { "Enable" : true, "CompareFunc" : "LessEqual" } + }, + + "DrawList" : "shadow", + + "RasterState" : + { + "depthBias" : "10", + "depthBiasSlopeScale" : "4" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/IndexedDataVector.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/IndexedDataVector.h index 37835bd6a8..82cc1e7d50 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/IndexedDataVector.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/IndexedDataVector.h @@ -30,6 +30,7 @@ namespace AZ::Render void Clear(); IndexType GetFreeSlotIndex(); void RemoveIndex(IndexType index); + void RemoveData(DataType* data); DataType& GetData(IndexType index); const DataType& GetData(IndexType index) const; @@ -42,6 +43,7 @@ namespace AZ::Render const AZStd::vector& GetIndexVector() const; IndexType GetRawIndex(IndexType index) const; + IndexType GetIndexForData(const DataType* data) const; private: constexpr static size_t InitialReservedSize = 128; diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/IndexedDataVector.inl b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/IndexedDataVector.inl index 076caad7f4..581186dbcc 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/IndexedDataVector.inl +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/IndexedDataVector.inl @@ -83,6 +83,16 @@ namespace AZ::Render m_indices.at(index) = m_firstFreeSlot; m_firstFreeSlot = index; } + + template + inline void IndexedDataVector::RemoveData(DataType* data) + { + IndexType indexForData = GetIndexForData(data); + if (indexForData != NoFreeSlot) + { + RemoveIndex(indexForData); + } + } template inline DataType& IndexedDataVector::GetData(IndexType index) @@ -131,4 +141,14 @@ namespace AZ::Render { return m_indices.at(index); } + + template + IndexType IndexedDataVector::GetIndexForData(const DataType* data) const + { + if (data >= &m_data.front() && data <= &m_data.back()) + { + return m_dataToIndices.at(data - &m_data.front()); + } + return NoFreeSlot; + } } // namespace AZ::Render diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/Icons/blank.png b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/Icons/blank.png new file mode 100644 index 0000000000..d040fa2e14 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/Icons/blank.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81b5fa1f978888c3be8a40fce20455668df2723a77587aeb7039f8bf74bdd0e3 +size 119 diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/Icons/changed_property.svg b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/Icons/changed_property.svg new file mode 100644 index 0000000000..c33e340a54 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/Icons/changed_property.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/InspectorWidget.qrc b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/InspectorWidget.qrc index 81a962801d..76733c52ed 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/InspectorWidget.qrc +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Inspector/InspectorWidget.qrc @@ -2,5 +2,7 @@ Icons/group_closed.png Icons/group_open.png + Icons/blank.png + Icons/changed_property.svg diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/Icons/skybox.svg b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/Icons/skybox.svg index 83df996198..a79bebdd46 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/Icons/skybox.svg +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/Icons/skybox.svg @@ -1,15 +1,6 @@ - - - - icon / Environmental / Sky Highlight - Created with Sketch. - - - - - - - - - - \ No newline at end of file + + + + + + diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.cpp index 025de21d31..e99f456653 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.cpp @@ -101,9 +101,9 @@ namespace MaterialEditor { if (IsInstanceNodePropertyModifed(node)) { - return ":/PropertyEditor/Resources/changed_data_item.png"; + return ":/Icons/changed_property.svg"; } - return ":/PropertyEditor/Resources/blank.png"; + return ":/Icons/blank.png"; } void MaterialInspector::AddOverviewGroup() diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/ToolBar/MaterialEditorToolBar.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/ToolBar/MaterialEditorToolBar.cpp index 1e189168da..10442e0c27 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/ToolBar/MaterialEditorToolBar.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/ToolBar/MaterialEditorToolBar.cpp @@ -88,18 +88,18 @@ namespace MaterialEditor toneMappingButton->setVisible(true); addWidget(toneMappingButton); - // Add model combo box - auto modelPresetComboBox = new ModelPresetComboBox(this); - modelPresetComboBox->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToContents); - modelPresetComboBox->view()->setMinimumWidth(200); - addWidget(modelPresetComboBox); - // Add lighting preset combo box auto lightingPresetComboBox = new LightingPresetComboBox(this); lightingPresetComboBox->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToContents); lightingPresetComboBox->view()->setMinimumWidth(200); addWidget(lightingPresetComboBox); + // Add model combo box + auto modelPresetComboBox = new ModelPresetComboBox(this); + modelPresetComboBox->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToContents); + modelPresetComboBox->view()->setMinimumWidth(200); + addWidget(modelPresetComboBox); + MaterialViewportNotificationBus::Handler::BusConnect(); } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp index 665adcd568..815f14aba7 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp @@ -521,9 +521,9 @@ namespace AZ { if (IsInstanceNodePropertyModifed(node)) { - return ":/PropertyEditor/Resources/changed_data_item.png"; + return ":/Icons/changed_property.svg"; } - return ":/PropertyEditor/Resources/blank.png"; + return ":/Icons/blank.png"; } bool MaterialPropertyInspector::SaveMaterial() const diff --git a/Gems/LyShine/Code/Source/Animation/UiAnimationSystem.cpp b/Gems/LyShine/Code/Source/Animation/UiAnimationSystem.cpp index cab6ae3fa6..ab45042b4c 100644 --- a/Gems/LyShine/Code/Source/Animation/UiAnimationSystem.cpp +++ b/Gems/LyShine/Code/Source/Animation/UiAnimationSystem.cpp @@ -37,11 +37,11 @@ namespace using UiAnimSystemUnorderedMap = AZStd::unordered_map; } // Serialization for anim nodes & param types -#define REGISTER_NODE_TYPE(name) assert(g_animNodeEnumToStringMap.contains(eUiAnimNodeType_ ## name)); \ +#define REGISTER_NODE_TYPE(name) assert(!g_animNodeEnumToStringMap.contains(eUiAnimNodeType_ ## name)); \ g_animNodeEnumToStringMap[eUiAnimNodeType_ ## name] = AZ_STRINGIZE(name); \ g_animNodeStringToEnumMap[UiAnimParamSystemString(AZ_STRINGIZE(name))] = eUiAnimNodeType_ ## name; -#define REGISTER_PARAM_TYPE(name) assert(g_animParamEnumToStringMap.contains(eUiAnimParamType_ ## name)); \ +#define REGISTER_PARAM_TYPE(name) assert(!g_animParamEnumToStringMap.contains(eUiAnimParamType_ ## name)); \ g_animParamEnumToStringMap[eUiAnimParamType_ ## name] = AZ_STRINGIZE(name); \ g_animParamStringToEnumMap[UiAnimParamSystemString(AZ_STRINGIZE(name))] = eUiAnimParamType_ ## name; diff --git a/Gems/Maestro/Code/Source/Cinematics/Movie.cpp b/Gems/Maestro/Code/Source/Cinematics/Movie.cpp index 9c4356560d..2a9a231177 100644 --- a/Gems/Maestro/Code/Source/Cinematics/Movie.cpp +++ b/Gems/Maestro/Code/Source/Cinematics/Movie.cpp @@ -87,11 +87,11 @@ namespace } // Serialization for anim nodes & param types -#define REGISTER_NODE_TYPE(name) assert(g_animNodeEnumToStringMap.contains(AnimNodeType::name)); \ +#define REGISTER_NODE_TYPE(name) assert(!g_animNodeEnumToStringMap.contains(AnimNodeType::name)); \ g_animNodeEnumToStringMap[AnimNodeType::name] = AZ_STRINGIZE(name); \ g_animNodeStringToEnumMap[AnimParamSystemString(AZ_STRINGIZE(name))] = AnimNodeType::name; -#define REGISTER_PARAM_TYPE(name) assert(g_animParamEnumToStringMap.contains(AnimParamType::name)); \ +#define REGISTER_PARAM_TYPE(name) assert(!g_animParamEnumToStringMap.contains(AnimParamType::name)); \ g_animParamEnumToStringMap[AnimParamType::name] = AZ_STRINGIZE(name); \ g_animParamStringToEnumMap[AnimParamSystemString(AZ_STRINGIZE(name))] = AnimParamType::name; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 35174e31fb..4fad5697c7 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -308,6 +308,12 @@ namespace Multiplayer return true; } + void MultiplayerSystemComponent::OnUpdateSessionBegin(const AzFramework::SessionConfig& sessionConfig, const AZStd::string& updateReason) + { + AZ_UNUSED(sessionConfig); + AZ_UNUSED(updateReason); + } + void MultiplayerSystemComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { const AZ::TimeMs deltaTimeMs = aznumeric_cast(static_cast(deltaTime * 1000.0f)); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index e46bbb59bc..ef1fb0da5b 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -70,6 +70,7 @@ namespace Multiplayer bool OnSessionHealthCheck() override; bool OnCreateSessionBegin(const AzFramework::SessionConfig& sessionConfig) override; bool OnDestroySessionBegin() override; + void OnUpdateSessionBegin(const AzFramework::SessionConfig& sessionConfig, const AZStd::string& updateReason) override; //! @} //! AZ::TickBus::Handler overrides. diff --git a/Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material b/Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material index c0de5969b2..d2acc2516a 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material +++ b/Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material @@ -4,12 +4,6 @@ "parentMaterial": "", "propertyLayoutVersion": 1, "properties": { - "macroColor": { - "useTexture": false - }, - "macroNormal": { - "useTexture": false - }, "baseColor": { "color": [ 0.18, 0.18, 0.18 ], "useTexture": false diff --git a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype index 191fa7a731..ec04412fe6 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype +++ b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype @@ -133,67 +133,6 @@ } } ], - "macroColor": [ - { - "id": "textureMap", - "displayName": "Texture", - "description": "Macro color texture map", - "type": "Image", - "connection": { - "type": "ShaderInput", - "id": "m_macroColorMap" - } - }, - { - "id": "useTexture", - "displayName": "Use Texture", - "description": "Whether to use the texture.", - "type": "Bool", - "defaultValue": true - } - - ], - "macroNormal": [ - { - "id": "textureMap", - "displayName": "Texture", - "description": "Macro normal texture map", - "type": "Image", - "connection": { - "type": "ShaderInput", - "id": "m_macroNormalMap" - } - }, - { - "id": "useTexture", - "displayName": "Use Texture", - "description": "Whether to use the texture.", - "type": "Bool", - "defaultValue": true - }, - { - "id": "flipX", - "displayName": "Flip X Channel", - "description": "Flip tangent direction for this normal map.", - "type": "Bool", - "defaultValue": false, - "connection": { - "type": "ShaderInput", - "id": "m_flipMacroNormalX" - } - }, - { - "id": "flipY", - "displayName": "Flip Y Channel", - "description": "Flip bitangent direction for this normal map.", - "type": "Bool", - "defaultValue": false, - "connection": { - "type": "ShaderInput", - "id": "m_flipMacroNormalY" - } - } - ], "baseColor": [ { "id": "color", @@ -380,22 +319,6 @@ } ], "functors": [ - { - "type": "UseTexture", - "args": { - "textureProperty": "macroColor.textureMap", - "useTextureProperty": "macroColor.useTexture", - "shaderOption": "o_macroColor_useTexture" - } - }, - { - "type": "UseTexture", - "args": { - "textureProperty": "macroNormal.textureMap", - "useTextureProperty": "macroNormal.useTexture", - "shaderOption": "o_macroNormal_useTexture" - } - }, { "type": "UseTexture", "args": { diff --git a/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype b/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype index 0a4f6d0229..3cdab8da10 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype +++ b/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype @@ -4,42 +4,59 @@ "version": 1, "groups": [ { - "id": "settings", - "displayName": "Settings" + "name": "baseColor", + "displayName": "Base Color", + "description": "Properties for configuring the surface reflected color for dielectrics or reflectance values for metals." + }, + { + "name": "normal", + "displayName": "Normal", + "description": "Properties related to configuring surface normal." } ], "properties": { - "macroColor": [ + "baseColor": [ { - "id": "useTexture", - "displayName": "Use Texture", - "description": "Whether to use the texture.", - "type": "Bool", - "defaultValue": true + "name": "textureMap", + "displayName": "Texture", + "description": "Base color of the macro material", + "type": "Image" } - ], - "macroNormal": [ + "normal": [ { - "id": "useTexture", - "displayName": "Use Texture", - "description": "Whether to use the texture.", + "name": "textureMap", + "displayName": "Texture", + "description": "Texture for defining surface normal direction. These will override normals generated from the geometry.", + "type": "Image" + }, + { + "name": "flipX", + "displayName": "Flip X Channel", + "description": "Flip tangent direction for this normal map.", "type": "Bool", - "defaultValue": true + "defaultValue": false + }, + { + "name": "flipY", + "displayName": "Flip Y Channel", + "description": "Flip bitangent direction for this normal map.", + "type": "Bool", + "defaultValue": false + }, + { + "name": "factor", + "displayName": "Factor", + "description": "Strength factor for scaling the values", + "type": "Float", + "defaultValue": 1.0, + "min": 0.0, + "softMax": 2.0 } ] } }, "shaders": [ - { - "file": "../../Shaders/Terrain/TerrainPBR_ForwardPass.shader" - }, - { - "file": "../../Shaders/Terrain/Terrain_Shadowmap.shader" - }, - { - "file": "../../Shaders/Terrain/Terrain_DepthPass.shader" - } ], "functors": [ ] diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli b/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli index 95d1a73bbd..72c1af953c 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli @@ -26,8 +26,24 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject float m_heightScale; }; + struct MacroMaterialData + { + float2 m_uvMin; + float2 m_uvMax; + float m_normalFactor; + bool m_flipNormalX; + bool m_flipNormalY; + uint m_mapsInUse; + }; + TerrainData m_terrainData; + MacroMaterialData m_macroMaterialData[4]; + uint m_macroMaterialCount; + + Texture2D m_macroColorMap[4]; + Texture2D m_macroNormalMap[4]; + // The below shouldn't be in this SRG but needs to be for now because the lighting functions depend on them. //! Reflection Probe (smallest probe volume that overlaps the object position) @@ -101,14 +117,6 @@ ShaderResourceGroup TerrainMaterialSrg : SRG_PerMaterial MaxAnisotropy = 16; }; - // Macro Color - Texture2D m_macroColorMap; - - // Macro normal - Texture2D m_macroNormalMap; - bool m_flipMacroNormalX; - bool m_flipMacroNormalY; - // Base Color float3 m_baseColor; float m_baseColorFactor; @@ -130,8 +138,6 @@ ShaderResourceGroup TerrainMaterialSrg : SRG_PerMaterial } option bool o_useTerrainSmoothing = false; -option bool o_macroColor_useTexture = true; -option bool o_macroNormal_useTexture = true; option bool o_baseColor_useTexture = true; option bool o_specularF0_useTexture = true; option bool o_normal_useTexture = true; diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl index fedd19a498..91e367500b 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl @@ -68,7 +68,6 @@ VSOutput TerrainPBR_MainPassVS(VertexInput IN) ForwardPassOutput TerrainPBR_MainPassPS(VSOutput IN) { // ------- Surface ------- - Surface surface; // Position @@ -83,12 +82,32 @@ ForwardPassOutput TerrainPBR_MainPassPS(VSOutput IN) // ------- Normal ------- float3 macroNormal = IN.m_normal; - if (o_macroNormal_useTexture) - { - macroNormal = GetNormalInputTS(TerrainMaterialSrg::m_macroNormalMap, TerrainMaterialSrg::m_sampler, - origUv, TerrainMaterialSrg::m_flipMacroNormalX, TerrainMaterialSrg::m_flipMacroNormalY, CreateIdentity3x3(), true, 1.0); - } + // ------- Macro Color / Normal ------- + float3 macroColor = TerrainMaterialSrg::m_baseColor.rgb; + [unroll] for (uint i = 0; i < 4; ++i) + { + float2 macroUvMin = ObjectSrg::m_macroMaterialData[i].m_uvMin; + float2 macroUvMax = ObjectSrg::m_macroMaterialData[i].m_uvMax; + float2 macroUv = lerp(macroUvMin, macroUvMax, IN.m_uv); + if (macroUv.x >= 0.0 && macroUv.x <= 1.0 && macroUv.y >= 0.0 && macroUv.y <= 1.0) + { + if ((ObjectSrg::m_macroMaterialData[i].m_mapsInUse & 1) > 0) + { + macroColor = GetBaseColorInput(ObjectSrg::m_macroColorMap[i], TerrainMaterialSrg::m_sampler, macroUv, macroColor, true); + } + if ((ObjectSrg::m_macroMaterialData[i].m_mapsInUse & 2) > 0) + { + bool flipX = ObjectSrg::m_macroMaterialData[i].m_flipNormalX; + bool flipY = ObjectSrg::m_macroMaterialData[i].m_flipNormalY; + bool factor = ObjectSrg::m_macroMaterialData[i].m_normalFactor; + macroNormal = GetNormalInputTS(ObjectSrg::m_macroNormalMap[i], TerrainMaterialSrg::m_sampler, + macroUv, flipX, flipY, CreateIdentity3x3(), true, factor); + } + break; + } + } + float3 detailNormal = GetNormalInputTS(TerrainMaterialSrg::m_normalMap, TerrainMaterialSrg::m_sampler, detailUv, TerrainMaterialSrg::m_flipNormalX, TerrainMaterialSrg::m_flipNormalY, CreateIdentity3x3(), o_normal_useTexture, TerrainMaterialSrg::m_normalFactor); @@ -97,9 +116,6 @@ ForwardPassOutput TerrainPBR_MainPassPS(VSOutput IN) surface.normal = normalize(surface.normal); surface.vertexNormal = normalize(IN.m_normal); - // ------- Macro Color ------- - float3 macroColor = GetBaseColorInput(TerrainMaterialSrg::m_macroColorMap, TerrainMaterialSrg::m_sampler, origUv, TerrainMaterialSrg::m_baseColor.rgb, o_baseColor_useTexture); - // ------- Base Color ------- float3 detailColor = GetBaseColorInput(TerrainMaterialSrg::m_baseColorMap, TerrainMaterialSrg::m_sampler, detailUv, TerrainMaterialSrg::m_baseColor.rgb, o_baseColor_useTexture); float3 blendedColor = BlendBaseColor(lerp(detailColor, TerrainMaterialSrg::m_baseColor.rgb, detailFactor), macroColor, TerrainMaterialSrg::m_baseColorFactor, o_baseColorTextureBlendMode, o_baseColor_useTexture); diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp index 560fe1eb60..d7300cdc48 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp @@ -49,13 +49,25 @@ namespace Terrain namespace MaterialInputs { + // Terrain material static const char* const HeightmapImage("settings.heightmapImage"); + + // Macro material + static const char* const MacroColorTextureMap("baseColor.textureMap"); + static const char* const MacroNormalTextureMap("normal.textureMap"); + static const char* const MacroNormalFlipX("normal.flipX"); + static const char* const MacroNormalFlipY("normal.flipY"); + static const char* const MacroNormalFactor("normal.factor"); } namespace ShaderInputs { static const char* const ModelToWorld("m_modelToWorld"); static const char* const TerrainData("m_terrainData"); + static const char* const MacroMaterialData("m_macroMaterialData"); + static const char* const MacroMaterialCount("m_macroMaterialCount"); + static const char* const MacroColorMap("m_macroColorMap"); + static const char* const MacroNormalMap("m_macroNormalMap"); } @@ -71,8 +83,6 @@ namespace Terrain void TerrainFeatureProcessor::Activate() { - m_areaData = {}; - m_dirtyRegion = AZ::Aabb::CreateNull(); Initialize(); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); } @@ -94,6 +104,10 @@ namespace Terrain { AZ_Error("TerrainFeatureProcessor", false, "No per-object ShaderResourceGroup found on terrain material."); } + else + { + PrepareMaterialData(); + } } } ); @@ -107,11 +121,17 @@ namespace Terrain void TerrainFeatureProcessor::Deactivate() { + TerrainMacroMaterialNotificationBus::Handler::BusDisconnect(); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect(); AZ::RPI::MaterialReloadNotificationBus::Handler::BusDisconnect(); m_patchModel = {}; m_areaData = {}; + m_dirtyRegion = AZ::Aabb::CreateNull(); + m_sectorData.clear(); + m_macroMaterials.Clear(); + m_materialAssetLoader = {}; + m_materialInstance = {}; } void TerrainFeatureProcessor::Render(const AZ::RPI::FeatureProcessor::RenderPacket& packet) @@ -126,7 +146,7 @@ namespace Terrain void TerrainFeatureProcessor::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) { - if (dataChangedMask != TerrainDataChangedMask::HeightData && dataChangedMask != TerrainDataChangedMask::Settings) + if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) == 0) { return; } @@ -140,14 +160,21 @@ namespace Terrain m_dirtyRegion.AddAabb(regionToUpdate); m_dirtyRegion.Clamp(worldBounds); - AZ::Transform transform = AZ::Transform::CreateTranslation(worldBounds.GetCenter()); + const AZ::Transform transform = AZ::Transform::CreateTranslation(worldBounds.GetCenter()); AZ::Vector2 queryResolution = AZ::Vector2(1.0f); AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( queryResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution); + // Sectors need to be rebuilt if the world bounds change in the x/y, or the sample spacing changes. + m_areaData.m_rebuildSectors = m_areaData.m_rebuildSectors || + m_areaData.m_terrainBounds.GetMin().GetX() != worldBounds.GetMin().GetX() || + m_areaData.m_terrainBounds.GetMin().GetY() != worldBounds.GetMin().GetY() || + m_areaData.m_terrainBounds.GetMax().GetX() != worldBounds.GetMax().GetX() || + m_areaData.m_terrainBounds.GetMax().GetY() != worldBounds.GetMax().GetY() || + m_areaData.m_sampleSpacing != queryResolution.GetX(); + m_areaData.m_transform = transform; - m_areaData.m_heightScale = worldBounds.GetZExtent(); m_areaData.m_terrainBounds = worldBounds; m_areaData.m_heightmapImageWidth = aznumeric_cast(worldBounds.GetXExtent() / queryResolution.GetX()); m_areaData.m_heightmapImageHeight = aznumeric_cast(worldBounds.GetYExtent() / queryResolution.GetY()); @@ -155,7 +182,93 @@ namespace Terrain m_areaData.m_updateHeight = aznumeric_cast(m_dirtyRegion.GetYExtent() / queryResolution.GetY()); // Currently query resolution is multidimensional but the rendering system only supports this changing in one dimension. m_areaData.m_sampleSpacing = queryResolution.GetX(); - m_areaData.m_propertiesDirty = true; + m_areaData.m_heightmapUpdated = true; + } + + void TerrainFeatureProcessor::OnTerrainMacroMaterialCreated(AZ::EntityId entityId, MaterialInstance material, const AZ::Aabb& region) + { + MacroMaterialData& materialData = FindOrCreateMacroMaterial(entityId); + materialData.m_bounds = region; + + UpdateMacroMaterialData(materialData, material); + + // Update all sectors in region. + ForOverlappingSectors(materialData.m_bounds, + [&](SectorData& sectorData) { + if (sectorData.m_macroMaterials.size() < sectorData.m_macroMaterials.max_size()) + { + sectorData.m_macroMaterials.push_back(m_macroMaterials.GetIndexForData(&materialData)); + } + } + ); + } + + void TerrainFeatureProcessor::OnTerrainMacroMaterialChanged(AZ::EntityId entityId, MaterialInstance macroMaterial) + { + if (macroMaterial) + { + MacroMaterialData& data = FindOrCreateMacroMaterial(entityId); + UpdateMacroMaterialData(data, macroMaterial); + } + else + { + RemoveMacroMaterial(entityId); + } + } + + void TerrainFeatureProcessor::OnTerrainMacroMaterialRegionChanged(AZ::EntityId entityId, [[maybe_unused]] const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) + { + MacroMaterialData& materialData = FindOrCreateMacroMaterial(entityId); + for (SectorData& sectorData : m_sectorData) + { + bool overlapsOld = sectorData.m_aabb.Overlaps(materialData.m_bounds); + bool overlapsNew = sectorData.m_aabb.Overlaps(newRegion); + if (overlapsOld && !overlapsNew) + { + // Remove the macro material from this sector + for (uint16_t& idx : sectorData.m_macroMaterials) + { + if (m_macroMaterials.GetData(idx).m_entityId == entityId) + { + idx = sectorData.m_macroMaterials.back(); + sectorData.m_macroMaterials.pop_back(); + } + } + } + else if (overlapsNew && !overlapsOld) + { + // Add the macro material to this sector + if (sectorData.m_macroMaterials.size() < MaxMaterialsPerSector) + { + sectorData.m_macroMaterials.push_back(m_macroMaterials.GetIndexForData(&materialData)); + } + } + } + m_areaData.m_macroMaterialsUpdated = true; + materialData.m_bounds = newRegion; + } + + void TerrainFeatureProcessor::OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId) + { + MacroMaterialData* materialData = FindMacroMaterial(entityId); + + if (materialData) + { + uint16_t destroyedMaterialIndex = m_macroMaterials.GetIndexForData(materialData); + ForOverlappingSectors(materialData->m_bounds, + [&](SectorData& sectorData) { + for (uint16_t& idx : sectorData.m_macroMaterials) + { + if (idx == destroyedMaterialIndex) + { + idx = sectorData.m_macroMaterials.back(); + sectorData.m_macroMaterials.pop_back(); + } + } + }); + } + + m_areaData.m_macroMaterialsUpdated = true; } void TerrainFeatureProcessor::UpdateTerrainData() @@ -165,9 +278,9 @@ namespace Terrain uint32_t width = m_areaData.m_updateWidth; uint32_t height = m_areaData.m_updateHeight; const AZ::Aabb& worldBounds = m_areaData.m_terrainBounds; - float queryResolution = m_areaData.m_sampleSpacing; + const float queryResolution = m_areaData.m_sampleSpacing; - AZ::RHI::Size worldSize = AZ::RHI::Size(m_areaData.m_heightmapImageWidth, m_areaData.m_heightmapImageHeight, 1); + const AZ::RHI::Size worldSize = AZ::RHI::Size(m_areaData.m_heightmapImageWidth, m_areaData.m_heightmapImageHeight, 1); if (!m_areaData.m_heightmapImage || m_areaData.m_heightmapImage->GetDescriptor().m_size != worldSize) { @@ -176,7 +289,7 @@ namespace Terrain height = worldSize.m_height; m_dirtyRegion = worldBounds; - AZ::Data::Instance imagePool = AZ::RPI::ImageSystemInterface::Get()->GetSystemAttachmentPool(); + const AZ::Data::Instance imagePool = AZ::RPI::ImageSystemInterface::Get()->GetSystemAttachmentPool(); AZ::RHI::ImageDescriptor imageDescriptor = AZ::RHI::ImageDescriptor::Create2D( AZ::RHI::ImageBindFlags::ShaderRead, width, height, AZ::RHI::Format::R16_UNORM ); @@ -210,9 +323,9 @@ namespace Terrain AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); - float clampedHeight = AZ::GetClamp((terrainHeight - worldBounds.GetMin().GetZ()) / worldBounds.GetExtents().GetZ(), 0.0f, 1.0f); - float expandedHeight = AZStd::roundf(clampedHeight * AZStd::numeric_limits::max()); - uint16_t uint16Height = aznumeric_cast(expandedHeight); + const float clampedHeight = AZ::GetClamp((terrainHeight - worldBounds.GetMin().GetZ()) / worldBounds.GetExtents().GetZ(), 0.0f, 1.0f); + const float expandedHeight = AZStd::roundf(clampedHeight * AZStd::numeric_limits::max()); + const uint16_t uint16Height = aznumeric_cast(expandedHeight); pixels.push_back(uint16Height); } @@ -241,118 +354,248 @@ namespace Terrain m_dirtyRegion = AZ::Aabb::CreateNull(); } + void TerrainFeatureProcessor::PrepareMaterialData() + { + const auto layout = m_materialInstance->GetAsset()->GetObjectSrgLayout(); + + m_modelToWorldIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::ModelToWorld)); + AZ_Error(TerrainFPName, m_modelToWorldIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::ModelToWorld); + + m_terrainDataIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::TerrainData)); + AZ_Error(TerrainFPName, m_terrainDataIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::TerrainData); + + m_macroMaterialDataIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::MacroMaterialData)); + AZ_Error(TerrainFPName, m_macroMaterialDataIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::MacroMaterialData); + + m_macroMaterialCountIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::MacroMaterialCount)); + AZ_Error(TerrainFPName, m_macroMaterialCountIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::MacroMaterialCount); + + m_macroColorMapIndex = layout->FindShaderInputImageIndex(AZ::Name(ShaderInputs::MacroColorMap)); + AZ_Error(TerrainFPName, m_macroColorMapIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::MacroColorMap); + + m_macroNormalMapIndex = layout->FindShaderInputImageIndex(AZ::Name(ShaderInputs::MacroNormalMap)); + AZ_Error(TerrainFPName, m_macroNormalMapIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::MacroNormalMap); + + m_heightmapPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::HeightmapImage)); + AZ_Error(TerrainFPName, m_heightmapPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::HeightmapImage); + + TerrainMacroMaterialRequestBus::EnumerateHandlers( + [&](TerrainMacroMaterialRequests* handler) + { + MaterialInstance macroMaterial; + AZ::Aabb bounds; + handler->GetTerrainMacroMaterialData(macroMaterial, bounds); + AZ::EntityId entityId = *(Terrain::TerrainMacroMaterialRequestBus::GetCurrentBusId()); + OnTerrainMacroMaterialCreated(entityId, macroMaterial, bounds); + return true; + } + ); + TerrainMacroMaterialNotificationBus::Handler::BusConnect(); + } + + void TerrainFeatureProcessor::UpdateMacroMaterialData(MacroMaterialData& macroMaterialData, MaterialInstance material) + { + // Since we're using an actual macro material instance for now, get the values from it that we care about. + const auto materialLayout = material->GetMaterialPropertiesLayout(); + + const AZ::RPI::MaterialPropertyIndex macroColorTextureMapIndex = materialLayout->FindPropertyIndex(AZ::Name(MaterialInputs::MacroColorTextureMap)); + AZ_Error(TerrainFPName, macroColorTextureMapIndex.IsValid(), "Failed to find shader input constant %s.", MaterialInputs::MacroColorTextureMap); + + const AZ::RPI::MaterialPropertyIndex macroNormalTextureMapIndex = materialLayout->FindPropertyIndex(AZ::Name(MaterialInputs::MacroNormalTextureMap)); + AZ_Error(TerrainFPName, macroNormalTextureMapIndex.IsValid(), "Failed to find shader input constant %s.", MaterialInputs::MacroNormalTextureMap); + + const AZ::RPI::MaterialPropertyIndex macroNormalFlipXIndex = materialLayout->FindPropertyIndex(AZ::Name(MaterialInputs::MacroNormalFlipX)); + AZ_Error(TerrainFPName, macroNormalFlipXIndex.IsValid(), "Failed to find shader input constant %s.", MaterialInputs::MacroNormalFlipX); + + const AZ::RPI::MaterialPropertyIndex macroNormalFlipYIndex = materialLayout->FindPropertyIndex(AZ::Name(MaterialInputs::MacroNormalFlipY)); + AZ_Error(TerrainFPName, macroNormalFlipYIndex.IsValid(), "Failed to find shader input constant %s.", MaterialInputs::MacroNormalFlipY); + + const AZ::RPI::MaterialPropertyIndex macroNormalFactorIndex = materialLayout->FindPropertyIndex(AZ::Name(MaterialInputs::MacroNormalFactor)); + AZ_Error(TerrainFPName, macroNormalFactorIndex.IsValid(), "Failed to find shader input constant %s.", MaterialInputs::MacroNormalFactor); + + macroMaterialData.m_colorImage = material->GetPropertyValue(macroColorTextureMapIndex).GetValue>(); + macroMaterialData.m_normalImage = material->GetPropertyValue(macroNormalTextureMapIndex).GetValue>(); + macroMaterialData.m_normalFlipX = material->GetPropertyValue(macroNormalFlipXIndex).GetValue(); + macroMaterialData.m_normalFlipY = material->GetPropertyValue(macroNormalFlipYIndex).GetValue(); + macroMaterialData.m_normalFactor = material->GetPropertyValue(macroNormalFactorIndex).GetValue(); + + if (macroMaterialData.m_bounds.IsValid()) + { + m_areaData.m_macroMaterialsUpdated = true; + } + } + void TerrainFeatureProcessor::ProcessSurfaces(const FeatureProcessor::RenderPacket& process) { AZ_PROFILE_FUNCTION(AzRender); + + const AZ::Aabb& terrainBounds = m_areaData.m_terrainBounds; - if (!m_areaData.m_terrainBounds.IsValid()) + if (!terrainBounds.IsValid()) { return; } - - if (m_areaData.m_propertiesDirty && m_materialInstance && m_materialInstance->CanCompile()) + + if (m_materialInstance && m_materialInstance->CanCompile()) { - UpdateTerrainData(); - - m_areaData.m_propertiesDirty = false; - m_sectorData.clear(); - - AZ::RPI::MaterialPropertyIndex heightmapPropertyIndex = - m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::HeightmapImage)); - AZ_Error(TerrainFPName, heightmapPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::HeightmapImage); - AZ::Data::Instance heightmapImage = m_areaData.m_heightmapImage; - m_materialInstance->SetPropertyValue(heightmapPropertyIndex, heightmapImage); - m_materialInstance->Compile(); - - const auto layout = m_materialInstance->GetAsset()->GetObjectSrgLayout(); - - m_modelToWorldIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::ModelToWorld)); - AZ_Error(TerrainFPName, m_modelToWorldIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::ModelToWorld); - - m_terrainDataIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::TerrainData)); - AZ_Error(TerrainFPName, m_terrainDataIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::TerrainData); - - float xFirstPatchStart = - m_areaData.m_terrainBounds.GetMin().GetX() - fmod(m_areaData.m_terrainBounds.GetMin().GetX(), GridMeters); - float xLastPatchStart = m_areaData.m_terrainBounds.GetMax().GetX() - fmod(m_areaData.m_terrainBounds.GetMax().GetX(), GridMeters); - float yFirstPatchStart = - m_areaData.m_terrainBounds.GetMin().GetY() - fmod(m_areaData.m_terrainBounds.GetMin().GetY(), GridMeters); - float yLastPatchStart = m_areaData.m_terrainBounds.GetMax().GetY() - fmod(m_areaData.m_terrainBounds.GetMax().GetY(), GridMeters); - - for (float yPatch = yFirstPatchStart; yPatch <= yLastPatchStart; yPatch += GridMeters) + if (m_areaData.m_rebuildSectors) { - for (float xPatch = xFirstPatchStart; xPatch <= xLastPatchStart; xPatch += GridMeters) + // Something about the whole world changed, so the sectors need to be rebuilt + + m_areaData.m_rebuildSectors = false; + + m_sectorData.clear(); + const float xFirstPatchStart = terrainBounds.GetMin().GetX() - fmod(terrainBounds.GetMin().GetX(), GridMeters); + const float xLastPatchStart = terrainBounds.GetMax().GetX() - fmod(terrainBounds.GetMax().GetX(), GridMeters); + const float yFirstPatchStart = terrainBounds.GetMin().GetY() - fmod(terrainBounds.GetMin().GetY(), GridMeters); + const float yLastPatchStart = terrainBounds.GetMax().GetY() - fmod(terrainBounds.GetMax().GetY(), GridMeters); + + const auto& materialAsset = m_materialInstance->GetAsset(); + const auto& shaderAsset = materialAsset->GetMaterialTypeAsset()->GetShaderAssetForObjectSrg(); + + for (float yPatch = yFirstPatchStart; yPatch <= yLastPatchStart; yPatch += GridMeters) { - const auto& materialAsset = m_materialInstance->GetAsset(); - auto& shaderAsset = materialAsset->GetMaterialTypeAsset()->GetShaderAssetForObjectSrg(); - auto objectSrg = AZ::RPI::ShaderResourceGroup::Create(shaderAsset, materialAsset->GetObjectSrgLayout()->GetName()); - if (!objectSrg) + for (float xPatch = xFirstPatchStart; xPatch <= xLastPatchStart; xPatch += GridMeters) { - AZ_Warning("TerrainFeatureProcessor", false, "Failed to create a new shader resource group, skipping."); - continue; - } - - { // Update SRG - - AZStd::array uvMin = { 0.0f, 0.0f }; - AZStd::array uvMax = { 1.0f, 1.0f }; - - uvMin[0] = (float)((xPatch - m_areaData.m_terrainBounds.GetMin().GetX()) / m_areaData.m_terrainBounds.GetXExtent()); - uvMin[1] = (float)((yPatch - m_areaData.m_terrainBounds.GetMin().GetY()) / m_areaData.m_terrainBounds.GetYExtent()); - - uvMax[0] = - (float)(((xPatch + GridMeters) - m_areaData.m_terrainBounds.GetMin().GetX()) / m_areaData.m_terrainBounds.GetXExtent()); - uvMax[1] = - (float)(((yPatch + GridMeters) - m_areaData.m_terrainBounds.GetMin().GetY()) / m_areaData.m_terrainBounds.GetYExtent()); - - AZStd::array uvStep = + auto objectSrg = AZ::RPI::ShaderResourceGroup::Create(shaderAsset, materialAsset->GetObjectSrgLayout()->GetName()); + if (!objectSrg) { - 1.0f / m_areaData.m_heightmapImageWidth, 1.0f / m_areaData.m_heightmapImageHeight, - }; - - AZ::Transform transform = m_areaData.m_transform; - transform.SetTranslation(xPatch, yPatch, m_areaData.m_transform.GetTranslation().GetZ()); - - AZ::Matrix3x4 matrix3x4 = AZ::Matrix3x4::CreateFromTransform(transform); - - objectSrg->SetConstant(m_modelToWorldIndex, matrix3x4); - - ShaderTerrainData terrainDataForSrg; - terrainDataForSrg.m_sampleSpacing = m_areaData.m_sampleSpacing; - terrainDataForSrg.m_heightScale = m_areaData.m_heightScale; - terrainDataForSrg.m_uvMin = uvMin; - terrainDataForSrg.m_uvMax = uvMax; - terrainDataForSrg.m_uvStep = uvStep; - objectSrg->SetConstant(m_terrainDataIndex, terrainDataForSrg); - - objectSrg->Compile(); - } - - m_sectorData.push_back(); - SectorData& sectorData = m_sectorData.back(); - - for (auto& lod : m_patchModel->GetLods()) - { - AZ::RPI::ModelLod& modelLod = *lod.get(); - sectorData.m_drawPackets.emplace_back(modelLod, 0, m_materialInstance, objectSrg); - AZ::RPI::MeshDrawPacket& drawPacket = sectorData.m_drawPackets.back(); - - // set the shader option to select forward pass IBL specular if necessary - if (!drawPacket.SetShaderOption(AZ::Name("o_meshUseForwardPassIBLSpecular"), AZ::RPI::ShaderOptionValue{ false })) - { - AZ_Warning("MeshDrawPacket", false, "Failed to set o_meshUseForwardPassIBLSpecular on mesh draw packet"); + AZ_Warning("TerrainFeatureProcessor", false, "Failed to create a new shader resource group, skipping."); + continue; } - uint8_t stencilRef = AZ::Render::StencilRefs::UseDiffuseGIPass | AZ::Render::StencilRefs::UseIBLSpecularPass; - drawPacket.SetStencilRef(stencilRef); - drawPacket.Update(*GetParentScene(), true); + + m_sectorData.push_back(); + SectorData& sectorData = m_sectorData.back(); + + for (auto& lod : m_patchModel->GetLods()) + { + AZ::RPI::ModelLod& modelLod = *lod.get(); + sectorData.m_drawPackets.emplace_back(modelLod, 0, m_materialInstance, objectSrg); + AZ::RPI::MeshDrawPacket& drawPacket = sectorData.m_drawPackets.back(); + + // set the shader option to select forward pass IBL specular if necessary + if (!drawPacket.SetShaderOption(AZ::Name("o_meshUseForwardPassIBLSpecular"), AZ::RPI::ShaderOptionValue{ false })) + { + AZ_Warning("MeshDrawPacket", false, "Failed to set o_meshUseForwardPassIBLSpecular on mesh draw packet"); + } + const uint8_t stencilRef = AZ::Render::StencilRefs::UseDiffuseGIPass | AZ::Render::StencilRefs::UseIBLSpecularPass; + drawPacket.SetStencilRef(stencilRef); + drawPacket.Update(*GetParentScene(), true); + } + + sectorData.m_aabb = + AZ::Aabb::CreateFromMinMax( + AZ::Vector3(xPatch, yPatch, terrainBounds.GetMin().GetZ()), + AZ::Vector3(xPatch + GridMeters, yPatch + GridMeters, terrainBounds.GetMax().GetZ()) + ); + sectorData.m_srg = objectSrg; + } + } + + if (m_areaData.m_macroMaterialsUpdated) + { + // sectors were rebuilt, so any cached macro material data needs to be regenerated + for (SectorData& sectorData : m_sectorData) + { + for (MacroMaterialData& macroMaterialData : m_macroMaterials.GetDataVector()) + { + if (macroMaterialData.m_bounds.Overlaps(sectorData.m_aabb)) + { + sectorData.m_macroMaterials.push_back(m_macroMaterials.GetIndexForData(¯oMaterialData)); + if (sectorData.m_macroMaterials.size() == MaxMaterialsPerSector) + { + break; + } + } + } + } + } + } + + if (m_areaData.m_heightmapUpdated) + { + UpdateTerrainData(); + + const AZ::Data::Instance heightmapImage = m_areaData.m_heightmapImage; + m_materialInstance->SetPropertyValue(m_heightmapPropertyIndex, heightmapImage); + m_materialInstance->Compile(); + } + + if (m_areaData.m_heightmapUpdated || m_areaData.m_macroMaterialsUpdated) + { + // Currently when anything in the heightmap changes we're updating all the srgs, but this could probably + // be optimized to only update the srgs that changed. + + m_areaData.m_heightmapUpdated = false; + m_areaData.m_macroMaterialsUpdated = false; + + for (SectorData& sectorData : m_sectorData) + { + ShaderTerrainData terrainDataForSrg; + + const float xPatch = sectorData.m_aabb.GetMin().GetX(); + const float yPatch = sectorData.m_aabb.GetMin().GetY(); + + terrainDataForSrg.m_uvMin = { + (xPatch - terrainBounds.GetMin().GetX()) / terrainBounds.GetXExtent(), + (yPatch - terrainBounds.GetMin().GetY()) / terrainBounds.GetYExtent() + }; + + terrainDataForSrg.m_uvMax = { + ((xPatch + GridMeters) - terrainBounds.GetMin().GetX()) / terrainBounds.GetXExtent(), + ((yPatch + GridMeters) - terrainBounds.GetMin().GetY()) / terrainBounds.GetYExtent() + }; + + terrainDataForSrg.m_uvStep = + { + 1.0f / m_areaData.m_heightmapImageWidth, + 1.0f / m_areaData.m_heightmapImageHeight, + }; + + AZ::Transform transform = m_areaData.m_transform; + transform.SetTranslation(xPatch, yPatch, m_areaData.m_transform.GetTranslation().GetZ()); + + terrainDataForSrg.m_sampleSpacing = m_areaData.m_sampleSpacing; + terrainDataForSrg.m_heightScale = terrainBounds.GetZExtent(); + + sectorData.m_srg->SetConstant(m_terrainDataIndex, terrainDataForSrg); + + AZStd::array macroMaterialData; + for (uint32_t i = 0; i < sectorData.m_macroMaterials.size(); ++i) + { + const MacroMaterialData& materialData = m_macroMaterials.GetData(sectorData.m_macroMaterials.at(i)); + ShaderMacroMaterialData& shaderData = macroMaterialData.at(i); + const AZ::Aabb& materialBounds = materialData.m_bounds; + + shaderData.m_uvMin = { + (xPatch - materialBounds.GetMin().GetX()) / materialBounds.GetXExtent(), + (yPatch - materialBounds.GetMin().GetY()) / materialBounds.GetYExtent() + }; + shaderData.m_uvMax = { + ((xPatch + GridMeters) - materialBounds.GetMin().GetX()) / materialBounds.GetXExtent(), + ((yPatch + GridMeters) - materialBounds.GetMin().GetY()) / materialBounds.GetYExtent() + }; + shaderData.m_normalFactor = materialData.m_normalFactor; + shaderData.m_flipNormalX = materialData.m_normalFlipX; + shaderData.m_flipNormalY = materialData.m_normalFlipY; + + const AZ::RHI::ImageView* colorImageView = materialData.m_colorImage ? materialData.m_colorImage->GetImageView() : nullptr; + sectorData.m_srg->SetImageView(m_macroColorMapIndex, colorImageView, i); + + const AZ::RHI::ImageView* normalImageView = materialData.m_normalImage ? materialData.m_normalImage->GetImageView() : nullptr; + sectorData.m_srg->SetImageView(m_macroNormalMapIndex, normalImageView, i); + + // set flags for which images are used. + shaderData.m_mapsInUse = (colorImageView ? ColorImageUsed : 0) | (normalImageView ? NormalImageUsed : 0); } - sectorData.m_aabb = - AZ::Aabb::CreateFromMinMax( - AZ::Vector3(xPatch, yPatch, m_areaData.m_terrainBounds.GetMin().GetZ()), - AZ::Vector3(xPatch + GridMeters, yPatch + GridMeters, m_areaData.m_terrainBounds.GetMax().GetZ()) - ); - sectorData.m_srg = objectSrg; + sectorData.m_srg->SetConstantArray(m_macroMaterialDataIndex, macroMaterialData); + sectorData.m_srg->SetConstant(m_macroMaterialCountIndex, aznumeric_cast(sectorData.m_macroMaterials.size())); + + const AZ::Matrix3x4 matrix3x4 = AZ::Matrix3x4::CreateFromTransform(transform); + sectorData.m_srg->SetConstant(m_modelToWorldIndex, matrix3x4); + + sectorData.m_srg->Compile(); } } } @@ -366,12 +609,20 @@ namespace Terrain { if ((view->GetUsageFlags() & AZ::RPI::View::UsageFlags::UsageCamera) > 0) { - AZ::Vector3 cameraPosition = view->GetCameraTransform().GetTranslation(); - AZ::Vector2 cameraPositionXY = AZ::Vector2(cameraPosition.GetX(), cameraPosition.GetY()); - AZ::Vector2 sectorCenterXY = AZ::Vector2(sectorData.m_aabb.GetCenter().GetX(), sectorData.m_aabb.GetCenter().GetY()); + const AZ::Vector3 cameraPosition = view->GetCameraTransform().GetTranslation(); + const AZ::Vector2 cameraPositionXY = AZ::Vector2(cameraPosition.GetX(), cameraPosition.GetY()); + const AZ::Vector2 sectorCenterXY = AZ::Vector2(sectorData.m_aabb.GetCenter().GetX(), sectorData.m_aabb.GetCenter().GetY()); - float sectorDistance = sectorCenterXY.GetDistance(cameraPositionXY); - float lodForCamera = floorf(AZ::GetMax(0.0f, log2f(sectorDistance / (GridMeters * 4.0f)))); + const float sectorDistance = sectorCenterXY.GetDistance(cameraPositionXY); + + // This will be configurable later + const float minDistanceForLod0 = (GridMeters * 4.0f); + + // For every distance doubling beyond a minDistanceForLod0, we only need half the mesh density. Each LOD + // is exactly half the resolution of the last. + const float lodForCamera = floorf(AZ::GetMax(0.0f, log2f(sectorDistance / minDistanceForLod0))); + + // All cameras should render the same LOD so effects like shadows are consistent. lodChoice = AZ::GetMin(lodChoice, aznumeric_cast(lodForCamera)); } } @@ -382,7 +633,7 @@ namespace Terrain AZ::Frustum viewFrustum = AZ::Frustum::CreateFromMatrixColumnMajor(view->GetWorldToClipMatrix()); if (viewFrustum.IntersectAabb(sectorData.m_aabb) != AZ::IntersectResult::Exterior) { - uint8_t lodToRender = AZ::GetMin(lodChoice, aznumeric_cast(sectorData.m_drawPackets.size() - 1)); + const uint8_t lodToRender = AZ::GetMin(lodChoice, aznumeric_cast(sectorData.m_drawPackets.size() - 1)); view->AddDrawPacket(sectorData.m_drawPackets.at(lodToRender).GetRHIDrawPacket()); } } @@ -395,9 +646,8 @@ namespace Terrain patchdata.m_uvs.clear(); patchdata.m_indices.clear(); - uint16_t gridVertices = gridSize + 1; // For m_gridSize quads, (m_gridSize + 1) vertices are needed. - size_t size = gridVertices * gridVertices; - size *= size; + const uint16_t gridVertices = gridSize + 1; // For m_gridSize quads, (m_gridSize + 1) vertices are needed. + const size_t size = gridVertices * gridVertices; patchdata.m_positions.reserve(size); patchdata.m_uvs.reserve(size); @@ -417,10 +667,10 @@ namespace Terrain { for (uint16_t x = 0; x < gridSize; ++x) { - uint16_t topLeft = y * gridVertices + x; - uint16_t topRight = topLeft + 1; - uint16_t bottomLeft = (y + 1) * gridVertices + x; - uint16_t bottomRight = bottomLeft + 1; + const uint16_t topLeft = y * gridVertices + x; + const uint16_t topRight = topLeft + 1; + const uint16_t bottomLeft = (y + 1) * gridVertices + x; + const uint16_t bottomRight = bottomLeft + 1; patchdata.m_indices.emplace_back(topLeft); patchdata.m_indices.emplace_back(topRight); @@ -469,14 +719,14 @@ namespace Terrain PatchData patchData; InitializeTerrainPatch(gridSize, gridSpacing, patchData); - auto positionBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_positions.size()), AZ::RHI::Format::R32G32_FLOAT); - auto positionsOutcome = CreateBufferAsset(patchData.m_positions.data(), positionBufferViewDesc, "TerrainPatchPositions"); + const auto positionBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_positions.size()), AZ::RHI::Format::R32G32_FLOAT); + const auto positionsOutcome = CreateBufferAsset(patchData.m_positions.data(), positionBufferViewDesc, "TerrainPatchPositions"); - auto uvBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_uvs.size()), AZ::RHI::Format::R32G32_FLOAT); - auto uvsOutcome = CreateBufferAsset(patchData.m_uvs.data(), uvBufferViewDesc, "TerrainPatchUvs"); + const auto uvBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_uvs.size()), AZ::RHI::Format::R32G32_FLOAT); + const auto uvsOutcome = CreateBufferAsset(patchData.m_uvs.data(), uvBufferViewDesc, "TerrainPatchUvs"); - auto indexBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_indices.size()), AZ::RHI::Format::R16_UINT); - auto indicesOutcome = CreateBufferAsset(patchData.m_indices.data(), indexBufferViewDesc, "TerrainPatchIndices"); + const auto indexBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_indices.size()), AZ::RHI::Format::R16_UINT); + const auto indicesOutcome = CreateBufferAsset(patchData.m_indices.data(), indexBufferViewDesc, "TerrainPatchIndices"); if (!positionsOutcome.IsSuccess() || !uvsOutcome.IsSuccess() || !indicesOutcome.IsSuccess()) { @@ -514,7 +764,7 @@ namespace Terrain return success; } - void TerrainFeatureProcessor::OnMaterialReinitialized([[maybe_unused]] const AZ::Data::Instance& material) + void TerrainFeatureProcessor::OnMaterialReinitialized([[maybe_unused]] const MaterialInstance& material) { for (auto& sectorData : m_sectorData) { @@ -530,4 +780,57 @@ namespace Terrain // This will control the max rendering size. Actual terrain size can be much // larger but this will limit how much is rendered. } + + TerrainFeatureProcessor::MacroMaterialData* TerrainFeatureProcessor::FindMacroMaterial(AZ::EntityId entityId) + { + for (MacroMaterialData& data : m_macroMaterials.GetDataVector()) + { + if (data.m_entityId == entityId) + { + return &data; + } + } + return nullptr; + } + + TerrainFeatureProcessor::MacroMaterialData& TerrainFeatureProcessor::FindOrCreateMacroMaterial(AZ::EntityId entityId) + { + MacroMaterialData* dataPtr = FindMacroMaterial(entityId); + if (dataPtr != nullptr) + { + return *dataPtr; + } + + const uint16_t slotId = m_macroMaterials.GetFreeSlotIndex(); + AZ_Assert(slotId != m_macroMaterials.NoFreeSlot, "Ran out of indices for macro materials"); + + MacroMaterialData& data = m_macroMaterials.GetData(slotId); + data.m_entityId = entityId; + return data; + } + + void TerrainFeatureProcessor::RemoveMacroMaterial(AZ::EntityId entityId) + { + for (MacroMaterialData& data : m_macroMaterials.GetDataVector()) + { + if (data.m_entityId == entityId) + { + m_macroMaterials.RemoveData(&data); + return; + } + } + AZ_Assert(false, "Entity Id not found in m_macroMaterials.") + } + + template + void TerrainFeatureProcessor::ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback) + { + for (SectorData& sectorData : m_sectorData) + { + if (sectorData.m_aabb.Overlaps(bounds)) + { + callback(sectorData); + } + } + } } diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h index d8df9b328c..d9f15f2d47 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h @@ -11,11 +11,13 @@ #include #include +#include #include #include #include #include +#include namespace AZ::RPI { @@ -25,6 +27,7 @@ namespace AZ::RPI } class Material; class Model; + class StreamingImage; } namespace Terrain @@ -33,6 +36,7 @@ namespace Terrain : public AZ::RPI::FeatureProcessor , private AZ::RPI::MaterialReloadNotificationBus::Handler , private AzFramework::Terrain::TerrainDataNotificationBus::Handler + , private TerrainMacroMaterialNotificationBus::Handler { public: AZ_RTTI(TerrainFeatureProcessor, "{D7DAC1F9-4A9F-4D3C-80AE-99579BF8AB1C}", AZ::RPI::FeatureProcessor); @@ -52,6 +56,15 @@ namespace Terrain void SetWorldSize(AZ::Vector2 sizeInMeters); private: + + using MaterialInstance = AZ::Data::Instance; + static constexpr uint32_t MaxMaterialsPerSector = 4; + + enum MacroMaterialFlags + { + ColorImageUsed = 0b01, + NormalImageUsed = 0b10, + }; struct ShaderTerrainData // Must align with struct in Object Srg { @@ -61,7 +74,17 @@ namespace Terrain float m_sampleSpacing; float m_heightScale; }; - + + struct ShaderMacroMaterialData + { + AZStd::array m_uvMin; + AZStd::array m_uvMax; + float m_normalFactor; + uint32_t m_flipNormalX{ 0 }; // bool in shader + uint32_t m_flipNormalY{ 0 }; // bool in shader + uint32_t m_mapsInUse{ 0b00 }; // 0b01 = color, 0b10 = normal + }; + struct VertexPosition { float m_posx; @@ -81,21 +104,56 @@ namespace Terrain AZStd::vector m_indices; }; + struct SectorData + { + AZ::Data::Instance m_srg; // Hold on to ref so it's not dropped + AZ::Aabb m_aabb; + AZStd::fixed_vector m_drawPackets; + AZStd::fixed_vector m_macroMaterials; + }; + + struct MacroMaterialData + { + AZ::EntityId m_entityId; + AZ::Aabb m_bounds = AZ::Aabb::CreateNull(); + + AZ::Data::Instance m_colorImage; + AZ::Data::Instance m_normalImage; + bool m_normalFlipX{ false }; + bool m_normalFlipY{ false }; + float m_normalFactor{ 0.0f }; + }; + // AZ::RPI::MaterialReloadNotificationBus::Handler overrides... - void OnMaterialReinitialized(const AZ::Data::Instance& material) override; + void OnMaterialReinitialized(const MaterialInstance& material) override; // AzFramework::Terrain::TerrainDataNotificationBus overrides... void OnTerrainDataDestroyBegin() override; void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override; + // TerrainMacroMaterialNotificationBus overrides... + void OnTerrainMacroMaterialCreated(AZ::EntityId entityId, MaterialInstance material, const AZ::Aabb& region) override; + void OnTerrainMacroMaterialChanged(AZ::EntityId entityId, MaterialInstance material) override; + void OnTerrainMacroMaterialRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) override; + void OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId) override; + void Initialize(); void InitializeTerrainPatch(uint16_t gridSize, float gridSpacing, PatchData& patchdata); bool InitializePatchModel(); void UpdateTerrainData(); + void PrepareMaterialData(); + void UpdateMacroMaterialData(MacroMaterialData& macroMaterialData, MaterialInstance material); void ProcessSurfaces(const FeatureProcessor::RenderPacket& process); + MacroMaterialData* FindMacroMaterial(AZ::EntityId entityId); + MacroMaterialData& FindOrCreateMacroMaterial(AZ::EntityId entityId); + void RemoveMacroMaterial(AZ::EntityId entityId); + + template + void ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback); + AZ::Outcome> CreateBufferAsset( const void* data, const AZ::RHI::BufferViewDescriptor& bufferViewDescriptor, const AZStd::string& bufferName); @@ -105,10 +163,15 @@ namespace Terrain static constexpr float GridMeters{ GridSpacing * GridSize }; AZStd::unique_ptr m_materialAssetLoader; - AZ::Data::Instance m_materialInstance; + MaterialInstance m_materialInstance; AZ::RHI::ShaderInputConstantIndex m_modelToWorldIndex; AZ::RHI::ShaderInputConstantIndex m_terrainDataIndex; + AZ::RHI::ShaderInputConstantIndex m_macroMaterialDataIndex; + AZ::RHI::ShaderInputConstantIndex m_macroMaterialCountIndex; + AZ::RHI::ShaderInputImageIndex m_macroColorMapIndex; + AZ::RHI::ShaderInputImageIndex m_macroNormalMapIndex; + AZ::RPI::MaterialPropertyIndex m_heightmapPropertyIndex; AZ::Data::Instance m_patchModel; @@ -117,26 +180,22 @@ namespace Terrain { AZ::Transform m_transform{ AZ::Transform::CreateIdentity() }; AZ::Aabb m_terrainBounds{ AZ::Aabb::CreateNull() }; - float m_heightScale{ 0.0f }; AZ::Data::Instance m_heightmapImage; uint32_t m_heightmapImageWidth{ 0 }; uint32_t m_heightmapImageHeight{ 0 }; uint32_t m_updateWidth{ 0 }; uint32_t m_updateHeight{ 0 }; - bool m_propertiesDirty{ true }; float m_sampleSpacing{ 0.0f }; + bool m_heightmapUpdated{ true }; + bool m_macroMaterialsUpdated{ true }; + bool m_rebuildSectors{ true }; }; TerrainAreaData m_areaData; AZ::Aabb m_dirtyRegion{ AZ::Aabb::CreateNull() }; - struct SectorData - { - AZ::Data::Instance m_srg; // Hold on to ref so it's not dropped - AZ::Aabb m_aabb; - AZStd::fixed_vector m_drawPackets; - }; - AZStd::vector m_sectorData; + + AZ::Render::IndexedDataVector m_macroMaterials; }; } diff --git a/Registry/AssetProcessorPlatformConfig.setreg b/Registry/AssetProcessorPlatformConfig.setreg index 3e90b063b5..ddc465c201 100644 --- a/Registry/AssetProcessorPlatformConfig.setreg +++ b/Registry/AssetProcessorPlatformConfig.setreg @@ -106,7 +106,7 @@ // "exclude": "mac" // } - "ScanFolder Game": { + "ScanFolder Project/Assets": { "watch": "@PROJECTROOT@", "display": "@PROJECTNAME@", "recursive": 1, @@ -129,6 +129,11 @@ "order": 30000, "include": "tools,renderer" }, + "ScanFolder Engine/Registry": { + "watch": "@ENGINEROOT@/Registry", + "recursive": 1, + "order": 40000 + }, // Excludes files that match the pattern or glob // if you use a pattern, remember to escape your backslashes (\\) diff --git a/Registry/bootstrap.setreg b/Registry/bootstrap.setreg index 3a15d59450..b2bcdd7f3f 100644 --- a/Registry/bootstrap.setreg +++ b/Registry/bootstrap.setreg @@ -17,7 +17,7 @@ "remote_port": 45643, "connect_to_remote": 0, "windows_connect_to_remote": 1, - "linux_connect_to_remote": 0, + "linux_connect_to_remote": 1, "provo_connect_to_remote": 1, "salem_connect_to_remote": 0, "jasper_connect_to_remote": 0, @@ -29,7 +29,7 @@ "salem_wait_for_connect": 0, "jasper_wait_for_connect": 0, "windows_wait_for_connect": 1, - "linux_wait_for_connect": 0, + "linux_wait_for_connect": 1, "android_wait_for_connect": 0, "ios_wait_for_connect": 0, "mac_wait_for_connect": 0, diff --git a/Templates/DefaultProject/Template/Registry/assets_scan_folders.setreg b/Templates/DefaultProject/Template/Registry/assets_scan_folders.setreg index a42f65efb4..05f6314da4 100644 --- a/Templates/DefaultProject/Template/Registry/assets_scan_folders.setreg +++ b/Templates/DefaultProject/Template/Registry/assets_scan_folders.setreg @@ -1,14 +1,21 @@ { - "Amazon": - { - "${Name}.Assets": - { - "SourcePaths": - [ - "Assets", - "ShaderLib", - "Shaders" - ] + "Amazon": { + "AssetProcessor": { + "ScanFolder Project/ShaderLib": { + "watch": "@PROJECTROOT@/ShaderLib", + "recursive": 1, + "order": 1 + }, + "ScanFolder Project/Shaders": { + "watch": "@PROJECTROOT@/Shaders", + "recurisve": 1, + "order": 2 + }, + "ScanFolder Project/Registry": { + "watch": "@PROJECTROOT@/Registry", + "recursive": 1, + "order": 3 + } } } -} \ No newline at end of file +} diff --git a/Templates/MinimalProject/Template/Registry/assets_scan_folders.setreg b/Templates/MinimalProject/Template/Registry/assets_scan_folders.setreg index a42f65efb4..05f6314da4 100644 --- a/Templates/MinimalProject/Template/Registry/assets_scan_folders.setreg +++ b/Templates/MinimalProject/Template/Registry/assets_scan_folders.setreg @@ -1,14 +1,21 @@ { - "Amazon": - { - "${Name}.Assets": - { - "SourcePaths": - [ - "Assets", - "ShaderLib", - "Shaders" - ] + "Amazon": { + "AssetProcessor": { + "ScanFolder Project/ShaderLib": { + "watch": "@PROJECTROOT@/ShaderLib", + "recursive": 1, + "order": 1 + }, + "ScanFolder Project/Shaders": { + "watch": "@PROJECTROOT@/Shaders", + "recurisve": 1, + "order": 2 + }, + "ScanFolder Project/Registry": { + "watch": "@PROJECTROOT@/Registry", + "recursive": 1, + "order": 3 + } } } -} \ No newline at end of file +} diff --git a/Tools/LyTestTools/ly_test_tools/o3de/asset_processor.py b/Tools/LyTestTools/ly_test_tools/o3de/asset_processor.py index 0623715350..682fcd3560 100644 --- a/Tools/LyTestTools/ly_test_tools/o3de/asset_processor.py +++ b/Tools/LyTestTools/ly_test_tools/o3de/asset_processor.py @@ -488,6 +488,9 @@ class AssetProcessor(object): logger.info(f"Launching AP with command: {command}") try: self._ap_proc = subprocess.Popen(command, cwd=ap_exe_path, env=process_utils.get_display_env()) + time.sleep(1) + if self._ap_proc.poll() is not None: + raise AssetProcessorError(f"AssetProcessor immediately quit with errorcode {self._ap_proc.returncode}") if accept_input: self.connect_control() @@ -506,10 +509,11 @@ class AssetProcessor(object): logger.exception("Exception while starting Asset Processor", be) # clean up to avoid leaking open AP process to future tests try: - self._ap_proc.kill() + if self._ap_proc: + self._ap_proc.kill() except Exception as ex: logger.exception("Ignoring exception while trying to terminate Asset Processor", ex) - raise # raise whatever prompted us to clean up + raise be # raise whatever prompted us to clean up def connect_listen(self, timeout=DEFAULT_TIMEOUT_SECONDS): # Wait for the AP we launched to be ready to accept a connection diff --git a/Tools/LyTestTools/tests/unit/test_asset_processor.py b/Tools/LyTestTools/tests/unit/test_asset_processor.py index aabbf2f2f5..743ca9a2e4 100755 --- a/Tools/LyTestTools/tests/unit/test_asset_processor.py +++ b/Tools/LyTestTools/tests/unit/test_asset_processor.py @@ -45,6 +45,7 @@ class TestAssetProcessor(object): @mock.patch('subprocess.Popen') @mock.patch('ly_test_tools.o3de.asset_processor.AssetProcessor.connect_socket') @mock.patch('ly_test_tools.o3de.asset_processor.ASSET_PROCESSOR_PLATFORM_MAP', {'foo': 'bar'}) + @mock.patch('time.sleep', mock.MagicMock()) def test_Start_NoneRunning_ProcStarted(self, mock_connect, mock_popen, mock_workspace): mock_ap_path = 'mock_ap_path' mock_workspace.asset_processor_platform = 'foo' @@ -54,6 +55,9 @@ class TestAssetProcessor(object): under_test = ly_test_tools.o3de.asset_processor.AssetProcessor(mock_workspace) under_test.enable_asset_processor_platform = mock.MagicMock() under_test.wait_for_idle = mock.MagicMock() + mock_proc_object = mock.MagicMock() + mock_proc_object.poll.return_value = None + mock_popen.return_value = mock_proc_object under_test.start(connect_to_ap=True) diff --git a/scripts/build/bootstrap/incremental_build_util.py b/scripts/build/bootstrap/incremental_build_util.py index 5c77559085..ff243ab02b 100644 --- a/scripts/build/bootstrap/incremental_build_util.py +++ b/scripts/build/bootstrap/incremental_build_util.py @@ -252,6 +252,18 @@ def find_snapshot_id(ec2_client, snapshot_hint, repository_name, project, pipeli snapshot_id = snapshot['SnapshotId'] return snapshot_id + +def offline_drive(disk_number=1): + """Use diskpart to offline a Windows drive""" + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(f""" + select disk {disk_number} + offline disk + """.encode('utf-8')) + subprocess.run(['diskpart', '/s', f.name]) + os.unlink(f.name) + + def create_volume(ec2_client, availability_zone, snapshot_hint, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): # The actual EBS default calculation for IOps is a floating point number, the closest approxmiation is 4x of the disk size for simplicity mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type) @@ -310,23 +322,26 @@ def create_volume(ec2_client, availability_zone, snapshot_hint, repository_name, def mount_volume_to_device(created): print('Mounting volume...') if os.name == 'nt': - f = tempfile.NamedTemporaryFile(delete=False) - f.write(""" - select disk 1 - online disk - attribute disk clear readonly - """.encode('utf-8')) # assume disk # for now + # Verify drive is in an offline state. + # Some Windows configs will automatically set new drives as online causing diskpart setup script to fail. + offline_drive() - if created: - print('Creating filesystem on new volume') - f.write("""create partition primary - select partition 1 - format quick fs=ntfs - assign - active - """.encode('utf-8')) + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(""" + select disk 1 + online disk + attribute disk clear readonly + """.encode('utf-8')) # assume disk # for now - f.close() + if created: + print('Creating filesystem on new volume') + f.write(""" + create partition primary + select partition 1 + format quick fs=ntfs + assign + active + """.encode('utf-8')) subprocess.call(['diskpart', '/s', f.name]) @@ -377,14 +392,7 @@ def unmount_volume_from_device(): print('Unmounting EBS volume from device...') if os.name == 'nt': kill_processes(MOUNT_PATH + 'workspace') - f = tempfile.NamedTemporaryFile(delete=False) - f.write(""" - select disk 1 - offline disk - """.encode('utf-8')) - f.close() - subprocess.call('diskpart /s %s' % f.name) - os.unlink(f.name) + offline_drive() else: kill_processes(MOUNT_PATH) subprocess.call(['umount', '-f', MOUNT_PATH])