Merge branch 'development' into Atom/guthadam/thumbnail_and_preview_refactor

monroegm-disable-blank-issue-2
Guthrie Adams 4 years ago
commit 8679f13bc2

@ -23,20 +23,20 @@ TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests")
class TestAtomEditorComponentsMain(object):
"""Holds tests for Atom components."""
@pytest.mark.test_case_id("C32078118") # Decal
@pytest.mark.test_case_id("C32078119") # DepthOfField
@pytest.mark.test_case_id("C32078120") # Directional Light
@pytest.mark.test_case_id("C32078121") # Exposure Control
@pytest.mark.test_case_id("C32078115") # Global Skylight (IBL)
@pytest.mark.test_case_id("C32078125") # Physical Sky
@pytest.mark.test_case_id("C32078127") # PostFX Layer
@pytest.mark.test_case_id("C32078131") # PostFX Radius Weight Modifier
@pytest.mark.test_case_id("C32078117") # Light
@pytest.mark.test_case_id("C36525660") # Display Mapper
def test_AtomEditorComponents_AddedToEntity(self, request, editor, level, workspace, project, launcher_platform):
"""
Please review the hydra script run by this test for more specific test info.
Tests the following Atom components and verifies all "expected_lines" appear in Editor.log:
1. Display Mapper
2. Light
3. PostFX Radius Weight Modifier
4. PostFX Layer
5. Physical Sky
6. Global Skylight (IBL)
7. Exposure Control
8. Directional Light
9. DepthOfField
10. Decal
Tests the Atom components & verifies all "expected_lines" appear in Editor.log
"""
cfg_args = [level]
@ -69,6 +69,18 @@ class TestAtomEditorComponentsMain(object):
"DepthOfField_test: Entity deleted: True",
"DepthOfField_test: UNDO entity deletion works: True",
"DepthOfField_test: REDO entity deletion works: True",
# Directional Light Component
"Directional Light Entity successfully created",
"Directional Light_test: Component added to the entity: True",
"Directional Light_test: Component removed after UNDO: True",
"Directional Light_test: Component added after REDO: True",
"Directional Light_test: Entered game mode: True",
"Directional Light_test: Exit game mode: True",
"Directional Light_test: Entity is hidden: True",
"Directional Light_test: Entity is shown: True",
"Directional Light_test: Entity deleted: True",
"Directional Light_test: UNDO entity deletion works: True",
"Directional Light_test: REDO entity deletion works: True",
# Exposure Control Component
"Exposure Control Entity successfully created",
"Exposure Control_test: Component added to the entity: True",
@ -180,6 +192,7 @@ class TestAtomEditorComponentsMain(object):
cfg_args=cfg_args,
)
@pytest.mark.test_case_id("C34525095")
def test_AtomEditorComponents_LightComponent(
self, request, editor, workspace, project, launcher_platform, level):
"""
@ -266,6 +279,15 @@ class TestMaterialEditorBasicTests(object):
request.addfinalizer(teardown)
@pytest.mark.parametrize("exe_file_name", ["MaterialEditor"])
@pytest.mark.test_case_id("C34448113") # Creating a New Asset.
@pytest.mark.test_case_id("C34448114") # Opening an Existing Asset.
@pytest.mark.test_case_id("C34448115") # Closing Selected Material.
@pytest.mark.test_case_id("C34448116") # Closing All Materials.
@pytest.mark.test_case_id("C34448117") # Closing all but Selected Material.
@pytest.mark.test_case_id("C34448118") # Saving Material.
@pytest.mark.test_case_id("C34448119") # Saving as a New Material.
@pytest.mark.test_case_id("C34448120") # Saving as a Child Material.
@pytest.mark.test_case_id("C34448121") # Saving all Open Materials.
def test_MaterialEditorBasicTests(
self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name):

@ -73,6 +73,7 @@ def create_screenshots_archive(screenshot_path):
class TestAllComponentsIndepthTests(object):
@pytest.mark.parametrize("screenshot_name", ["AtomBasicLevelSetup.ppm"])
@pytest.mark.test_case_id("C34603773")
def test_BasicLevelSetup_SetsUpLevel(
self, request, editor, workspace, project, launcher_platform, level, screenshot_name):
"""
@ -115,6 +116,7 @@ class TestAllComponentsIndepthTests(object):
create_screenshots_archive(screenshot_directory)
@pytest.mark.test_case_id("C34525095")
def test_LightComponent_ScreenshotMatchesGoldenImage(
self, request, editor, workspace, project, launcher_platform, level):
"""
@ -225,6 +227,8 @@ class TestMaterialEditor(object):
pytest.param("-rhi=Vulkan", ["Registering vulkan RHI"])
])
@pytest.mark.parametrize("exe_file_name", ["MaterialEditor"])
@pytest.mark.test_case_id("C30973986") # Material Editor Launching in Dx12
@pytest.mark.test_case_id("C30973987") # Material Editor Launching in Vulkan
def test_MaterialEditorLaunch_AllRHIOptionsSucceed(
self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name, cfg_args,
expected_lines):

@ -23,6 +23,7 @@ class TestAutomation(EditorTestSuite):
# Remove -autotest_mode from global_extra_cmdline_args since we need rendering for these tests.
global_extra_cmdline_args = ["-BatchMode"] # Default is ["-BatchMode", "-autotest_mode"]
@pytest.mark.test_case_id("C34603773")
class AtomGPU_BasicLevelSetup_SetsUpLevel(EditorSharedTest):
use_null_renderer = False # Default is True
screenshot_name = "AtomBasicLevelSetup.ppm"

@ -30,6 +30,7 @@ class TestAtomEditorComponentsSandbox(object):
class TestAtomEditorComponentsMain(object):
"""Holds tests for Atom components."""
@pytest.mark.test_case_id("C32078128")
def test_AtomEditorComponents_ReflectionProbeAddedToEntity(
self, request, editor, level, workspace, project, launcher_platform):
"""

@ -52,9 +52,6 @@ def Joints_BallLeadFollowerCollide():
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import azlmbr.legacy.general as general
import azlmbr.bus
from JointsHelper import JointEntityCollisionAware
# Helper Entity class - self.collided flag is set when instance receives collision event.
@ -75,17 +72,12 @@ def Joints_BallLeadFollowerCollide():
lead = Entity("lead")
follower = Entity("follower")
# 4) Wait for several seconds
general.idle_wait(2.0) # wait for lead and follower to move
# 5) Check to see if lead and follower behaved as expected
Report.critical_result(Tests.check_collision_happened, lead.collided and follower.collided)
# 4) Wait for collision between lead and follower or timeout
Report.critical_result(Tests.check_collision_happened, helper.wait_for_condition(lambda: lead.collided and follower.collided, 10.0))
# 6) Exit Game Mode
# 5) Exit Game Mode
helper.exit_game_mode(Tests.exit_game_mode)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(Joints_BallLeadFollowerCollide)

@ -52,11 +52,13 @@ def Joints_HingeNoLimitsConstrained():
"""
import os
import sys
import math
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import azlmbr.legacy.general as general
import azlmbr.bus
import azlmbr.math as azmath
import JointsHelper
from JointsHelper import JointEntity
@ -84,28 +86,65 @@ def Joints_HingeNoLimitsConstrained():
Report.info_vector3(lead.position, "lead initial position:")
Report.info_vector3(follower.position, "follower initial position:")
leadInitialPosition = lead.position
followerInitialPosition = follower.position
# 4) Wait for several seconds
general.idle_wait(4.0) # wait for lead and follower to move
# 4) Wait for the follower to move above and over the lead or Timeout
normalizedStartPos = JointsHelper.getRelativeVector(lead.position, follower.position)
normalizedStartPos = normalizedStartPos.GetNormalizedSafe()
class WaitCondition:
ANGLE_CHECKPOINT_1 = math.radians(90)
ANGLE_CHECKPOINT_2 = math.radians(200)
angleAchieved = 0.0
followerMovedAbove90Deg = False #this is expected to be true to pass the test
followerMovedAbove200Deg = False #this is expected to be true to pass the test
jointNormal = azmath.Vector3(0.0, -1.0, 0.0) # the joint rotates around the y axis
def checkConditionMet(self):
#calculate the current follower-lead vector
normalVec = JointsHelper.getRelativeVector(lead.position, follower.position)
normalVec = normalVec.GetNormalizedSafe()
#triple product and get the angle
tripleProduct = normalizedStartPos.dot(normalVec.cross(self.jointNormal))
currentAngle = math.acos(normalizedStartPos.Dot(normalVec))
if tripleProduct < 0:
currentAngle = (2*math.pi) - currentAngle
#if the angle is now less then last time, it is no longer rising, so end the test.
if currentAngle < self.angleAchieved:
return True
#once we're passed the final check point, end the test
if currentAngle > self.ANGLE_CHECKPOINT_2:
self.followerMovedAbove200Deg = True
return True
self.angleAchieved = currentAngle
self.followerMovedAbove90Deg = currentAngle > self.ANGLE_CHECKPOINT_1
return False
def isFollowerPositionCorrect(self):
return self.followerMovedAbove90Deg and self.followerMovedAbove200Deg
waitCondition = WaitCondition()
MAX_WAIT_TIME = 10.0 #seconds
conditionMet = helper.wait_for_condition(lambda: waitCondition.checkConditionMet(), MAX_WAIT_TIME)
# 5) Check to see if lead and follower behaved as expected
Report.info_vector3(lead.position, "lead position after 1 second:")
Report.info_vector3(follower.position, "follower position after 1 second:")
Report.info_vector3(lead.position, "lead position after test run:")
Report.info_vector3(follower.position, "follower position after test run:")
leadPositionDelta = lead.position.Subtract(leadInitialPosition)
leadRemainedStill = JointsHelper.vector3SmallerThanScalar(leadPositionDelta, FLOAT_EPSILON)
Report.critical_result(Tests.check_lead_position, leadRemainedStill)
followerSwingedOverLead = (follower.position.x < leadInitialPosition.x and
follower.position.z > leadInitialPosition.z)
Report.critical_result(Tests.check_follower_position, followerSwingedOverLead)
Report.critical_result(Tests.check_follower_position, conditionMet and waitCondition.isFollowerPositionCorrect())
# 6) Exit Game Mode
helper.exit_game_mode(Tests.exit_game_mode)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(Joints_HingeNoLimitsConstrained)

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:817bd8dd22e185136418b60fba5b3552993687515d3d5ae96791f2c3be907b92
size 7233
oid sha256:60276c07b45a734e4f71d695278167ea61e884f8b513906168c9642078ad5954
size 6045

@ -102,7 +102,7 @@ ly_add_target(
3rdParty::Qt::Gui
3rdParty::Qt::Widgets
3rdParty::Qt::Concurrent
3rdParty::tiff
3rdParty::TIFF
3rdParty::squish-ccr
3rdParty::AWSNativeSDK::STS
Legacy::CryCommon

@ -77,9 +77,11 @@ namespace AZ
public:
/**
* Allocator used by the EBus.
* The default setting is AZStd::allocator, which uses AZ::SystemAllocator.
* The default setting is Internal EBusEnvironmentAllocator
* EBus code stores their Context instances in static memory
* Therfore the configured allocator must last as long as the EBus in a module
*/
using AllocatorType = AZStd::allocator;
using AllocatorType = AZ::Internal::EBusEnvironmentAllocator;
/**
* Defines how many handlers can connect to an address on the EBus

@ -34,7 +34,9 @@ namespace AZ
friend IAllocator;
friend class AllocatorBase;
friend class Debug::AllocationRecords;
friend class AZ::Internal::EnvironmentVariableHolder<AllocatorManager>;
template<typename T, typename... Args> friend constexpr auto AZStd::construct_at(T*, Args&&... args)
->AZStd::enable_if_t<AZStd::is_void_v<AZStd::void_t<decltype(new (AZStd::declval<void*>()) T(AZStd::forward<Args>(args)...))>>, T*>;
template<typename T> constexpr friend void AZStd::destroy_at(T*);
public:
typedef AZStd::function<void (IAllocator* allocator, size_t /*byteSize*/, size_t /*alignment*/, int/* flags*/, const char* /*name*/, const char* /*fileName*/, int lineNum /*=0*/)> OutOfMemoryCBType;

@ -251,16 +251,15 @@ namespace AZ
class EnvironmentVariableHolder
: public EnvironmentVariableHolderBase
{
void ConstructImpl(const AZStd::true_type& /* AZStd::has_trivial_constructor<T> */)
{
memset(&m_value, 0, sizeof(T));
}
template<class... Args>
void ConstructImpl(const AZStd::false_type& /* AZStd::has_trivial_constructor<T> */, Args&&... args)
void ConstructImpl(Args&&... args)
{
// Construction of non-trivial types is left up to the type's constructor.
new(&m_value) T(AZStd::forward<Args>(args)...);
// Use std::launder to ensure that the compiler treats the T* reinterpret_cast as a new object
#if __cpp_lib_launder
AZStd::construct_at(std::launder(reinterpret_cast<T*>(&m_value)), AZStd::forward<Args>(args)...);
#else
AZStd::construct_at(reinterpret_cast<T*>(&m_value), AZStd::forward<Args>(args)...);
#endif
}
static void DestructDispatchNoLock(EnvironmentVariableHolderBase *base, DestroyTarget selfDestruct)
{
@ -274,10 +273,12 @@ namespace AZ
AZ_Assert(self->m_isConstructed, "Variable is not constructed. Please check your logic and guard if needed!");
self->m_isConstructed = false;
self->m_moduleOwner = nullptr;
if constexpr(!AZStd::is_trivially_destructible_v<T>)
{
reinterpret_cast<T*>(&self->m_value)->~T();
}
// Use std::launder to ensure that the compiler treats the T* reinterpret_cast as a new object
#if __cpp_lib_launder
AZStd::destroy_at(std::launder(reinterpret_cast<T*>(&self->m_value)));
#else
AZStd::destroy_at(reinterpret_cast<T*>(&self->m_value));
#endif
}
public:
EnvironmentVariableHolder(u32 guid, bool isOwnershipTransfer, Environment::AllocatorInterface* allocator)
@ -303,24 +304,13 @@ namespace AZ
UnregisterAndDestroy(DestructDispatchNoLock, moduleRelease);
}
void Construct()
{
AZStd::lock_guard<AZStd::spin_mutex> lock(m_mutex);
if (!m_isConstructed)
{
ConstructImpl(AZStd::is_trivially_constructible<T>{});
m_isConstructed = true;
m_moduleOwner = Environment::GetModuleId();
}
}
template <class... Args>
void Construct(Args&&... args)
{
AZStd::lock_guard<AZStd::spin_mutex> lock(m_mutex);
if (!m_isConstructed)
{
ConstructImpl(typename AZStd::false_type(), AZStd::forward<Args>(args)...);
ConstructImpl(AZStd::forward<Args>(args)...);
m_isConstructed = true;
m_moduleOwner = Environment::GetModuleId();
}
@ -333,7 +323,7 @@ namespace AZ
}
// variable storage
typename AZStd::aligned_storage<sizeof(T), AZStd::alignment_of<T>::value>::type m_value;
AZStd::aligned_storage_for_t<T> m_value;
static int s_moduleUseCount;
};
@ -468,6 +458,11 @@ namespace AZ
Get() = value;
}
void Set(T&& value)
{
Get() = AZStd::move(value);
}
explicit operator bool() const
{
return IsValid();

@ -42,7 +42,10 @@ namespace AZ
AZ_Assert(m_useCount > 0, "m_useCount is already 0!");
if (m_useCount.fetch_sub(1) == 1)
{
AZ::NameDictionary::Instance().TryReleaseName(hash);
if (AZ::NameDictionary::IsReady())
{
AZ::NameDictionary::Instance().TryReleaseName(hash);
}
}
}
}

@ -21,23 +21,18 @@ namespace AZ
namespace NameDictionaryInternal
{
static AZ::EnvironmentVariable<NameDictionary*> s_instance = nullptr;
static AZ::EnvironmentVariable<NameDictionary> s_instance = nullptr;
}
void NameDictionary::Create()
{
using namespace NameDictionaryInternal;
AZ_Assert(!s_instance || !s_instance.Get(), "NameDictionary already created!");
AZ_Assert(!s_instance, "NameDictionary already created!");
if (!s_instance)
{
s_instance = AZ::Environment::CreateVariable<NameDictionary*>(NameDictionaryInstanceName);
}
if (!s_instance.Get())
{
s_instance.Set(aznew NameDictionary());
s_instance = AZ::Environment::CreateVariable<NameDictionary>(NameDictionaryInstanceName);
}
}
@ -46,8 +41,7 @@ namespace AZ
using namespace NameDictionaryInternal;
AZ_Assert(s_instance, "NameDictionary not created!");
delete (*s_instance);
*s_instance = nullptr;
s_instance.Reset();
}
bool NameDictionary::IsReady()
@ -56,10 +50,10 @@ namespace AZ
if (!s_instance)
{
s_instance = Environment::FindVariable<NameDictionary*>(NameDictionaryInstanceName);
s_instance = Environment::FindVariable<NameDictionary>(NameDictionaryInstanceName);
}
return s_instance && *s_instance;
return s_instance.IsConstructed();
}
NameDictionary& NameDictionary::Instance()
@ -68,12 +62,12 @@ namespace AZ
if (!s_instance)
{
s_instance = Environment::FindVariable<NameDictionary*>(NameDictionaryInstanceName);
s_instance = Environment::FindVariable<NameDictionary>(NameDictionaryInstanceName);
}
AZ_Assert(s_instance && *s_instance, "NameDictionary has not been initialized yet.");
AZ_Assert(s_instance.IsConstructed(), "NameDictionary has not been initialized yet.");
return *(*s_instance);
return *s_instance;
}
NameDictionary::NameDictionary()

@ -16,7 +16,7 @@
#include <AzCore/Memory/OSAllocator.h>
#include <AzCore/Name/Name.h>
namespace MaterialEditor
namespace MaterialEditor
{
class MaterialEditorCoreComponent;
}
@ -34,14 +34,14 @@ namespace AZ
{
class NameData;
};
//! Maintains a list of unique strings for Name objects.
//! The main benefit of the Name system is very fast string equality comparison, because every
//! unique name has a unique ID. The NameDictionary's purpose is to guarantee name IDs do not
//! unique name has a unique ID. The NameDictionary's purpose is to guarantee name IDs do not
//! collide. It also saves memory by removing duplicate strings.
//!
//! Benchmarks have shown that creating a new Name object can be quite slow when the name doesn't
//! already exist in the NameDictionary, but is comparable to creating an AZStd::string for names
//! Benchmarks have shown that creating a new Name object can be quite slow when the name doesn't
//! already exist in the NameDictionary, but is comparable to creating an AZStd::string for names
//! that already exist.
class NameDictionary final
{
@ -51,7 +51,10 @@ namespace AZ
friend Name;
friend Internal::NameData;
friend UnitTest::NameDictionaryTester;
template<typename T, typename... Args> friend constexpr auto AZStd::construct_at(T*, Args&&... args)
-> AZStd::enable_if_t<AZStd::is_void_v<AZStd::void_t<decltype(new (AZStd::declval<void*>()) T(AZStd::forward<Args>(args)...))>>, T*>;
template<typename T> constexpr friend void AZStd::destroy_at(T*);
public:
static void Create();
@ -62,7 +65,7 @@ namespace AZ
//! Makes a Name from the provided raw string. If an entry already exists in the dictionary, it is shared.
//! Otherwise, it is added to the internal dictionary.
//!
//!
//! @param name The name to resolve against the dictionary.
//! @return A Name instance holding a dictionary entry associated with the provided raw string.
Name MakeName(AZStd::string_view name);
@ -84,13 +87,13 @@ namespace AZ
// Attempts to release the name from the dictionary, but checks to make sure
// a reference wasn't taken by another thread.
void TryReleaseName(Name::Hash hash);
//////////////////////////////////////////////////////////////////////////
// Calculates a hash for the provided name string.
// Does not attempt to resolve hash collisions; that is handled elsewhere.
Name::Hash CalcHash(AZStd::string_view name);
AZStd::unordered_map<Name::Hash, Internal::NameData*> m_dictionary;
mutable AZStd::shared_mutex m_sharedMutex;
};

@ -0,0 +1,94 @@
/*
* 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 <AzCore/std/allocator_stateless.h>
#include <AzCore/Memory/OSAllocator.h>
namespace AZStd
{
stateless_allocator::stateless_allocator(const char* name)
: m_name(name) {}
const char* stateless_allocator::get_name() const
{
return m_name;
}
void stateless_allocator::set_name(const char* name)
{
m_name = name;
}
auto stateless_allocator::allocate(size_type byteSize) -> pointer_type
{
return allocate(byteSize, AZ_DEFAULT_ALIGNMENT, 0);
}
auto stateless_allocator::allocate(size_type byteSize, size_type alignment, int) -> pointer_type
{
pointer_type address = AZ_OS_MALLOC(byteSize, alignment);
if (address == nullptr)
{
AZ_Error("Memory", false, "stateless_allocator ran out of system memory!\n");
}
return address;
}
void stateless_allocator::deallocate(pointer_type ptr, size_type)
{
AZ_OS_FREE(ptr);
}
void stateless_allocator::deallocate(pointer_type ptr, size_type, size_type)
{
AZ_OS_FREE(ptr);
}
auto stateless_allocator::max_size() const -> size_type
{
return AZ_CORE_MAX_ALLOCATOR_SIZE;
}
stateless_allocator stateless_allocator::select_on_container_copy_construction() const
{
return *this;
}
auto stateless_allocator::resize(pointer_type, size_type) -> size_type
{
return 0;
}
bool stateless_allocator::is_lock_free()
{
return false;
}
bool stateless_allocator::is_stale_read_allowed()
{
return false;
}
bool stateless_allocator::is_delayed_recycling()
{
return false;
}
// comparison operators
bool operator==(const stateless_allocator&, const stateless_allocator&)
{
return true;
}
bool operator!=(const stateless_allocator&, const stateless_allocator&)
{
return false;
}
}

@ -0,0 +1,61 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/std/base.h>
#include <AzCore/std/typetraits/integral_constant.h>
#include <AzCore/RTTI/TypeInfoSimple.h>
namespace AZStd
{
class stateless_allocator
{
public:
AZ_TYPE_INFO(stateless_allocator, "{E4976C53-0B20-4F39-8D41-0A76F59A7D68}");
using value_type = uint8_t;
using pointer_type = void*;
using size_type = size_t;
using difference_type = ptrdiff_t;
using allow_memory_leaks = AZStd::true_type;
stateless_allocator(const char* name = "AZStd::stateless_allocator");
stateless_allocator(const stateless_allocator& rhs) = default;
stateless_allocator& operator=(const stateless_allocator& rhs) = default;
const char* get_name() const;
void set_name(const char* name);
pointer_type allocate(size_type byteSize);
pointer_type allocate(size_type byteSize, size_type alignment, int flags = 0);
void deallocate(pointer_type ptr, size_type alignment);
void deallocate(pointer_type ptr, size_type byteSize, size_type alignment);
// max_size actually returns the true maximum size of a single allocation
size_type max_size() const;
// Returns a copy of the allocator
stateless_allocator select_on_container_copy_construction() const;
//! extensions
size_type resize(pointer_type ptr, size_type newSize);
bool is_lock_free();
bool is_stale_read_allowed();
bool is_delayed_recycling();
private:
const char* m_name;
};
bool operator==(const stateless_allocator& left, const stateless_allocator& right);
bool operator!=(const stateless_allocator& left, const stateless_allocator& right);
}

@ -12,6 +12,8 @@ set(FILES
allocator.h
allocator_ref.h
allocator_stack.h
allocator_stateless.cpp
allocator_stateless.h
allocator_static.h
allocator_traits.h
any.h

@ -20,7 +20,7 @@
namespace AZStd
{
// alias std::pointer_traits into the AZStd::namespace
// alias std::pointer_traits into the AZStd::namespace
using std::pointer_traits;
//! Bring the names of uninitialized_default_construct and
@ -229,7 +229,7 @@ namespace AZStd
//! `new (declval<void*>()) T(declval<Args>()...)` is well-formed
template <typename T, typename... Args>
constexpr auto construct_at(T* ptr, Args&&... args)
-> enable_if_t<is_void_v<void_t<decltype(new (declval<void*>()) T(AZStd::forward<Args>(args)...))>>, T*>
-> enable_if_t<AZStd::is_void_v<AZStd::void_t<decltype(new (AZStd::declval<void*>()) T(AZStd::forward<Args>(args)...))>>, T*>
{
return ::new (ptr) T(AZStd::forward<Args>(args)...);
}
@ -487,7 +487,7 @@ namespace AZStd
{
//! Implements the C++17 uninitialized_move function
//! The functions accepts two input iterators and an output iterator
//! It performs an AZStd::move on each in in the range of the input iterator
//! It performs an AZStd::move on each in in the range of the input iterator
//! and stores the result in location pointed by the output iterator
template <typename InputIt, typename ForwardIt>
ForwardIt uninitialized_move(InputIt first, InputIt last, ForwardIt result)

@ -29,6 +29,10 @@ namespace AzFramework
AZStd::vector<AZ::IO::Path> 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.

@ -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,

@ -35,6 +35,9 @@ namespace AzFramework
// A collection of custom properties for a session.
AZStd::unordered_map<AZStd::string, AZStd::string> 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;

@ -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<SessionNotifications>;
} // namespace AzFramework

@ -96,6 +96,8 @@ namespace AzGameFramework
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_CommandLine(registry, m_commandLine, false);
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_ProjectUserRegistry(registry, AZ_TRAIT_OS_PLATFORM_CODENAME, specializations, &scratchBuffer);
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_CommandLine(registry, m_commandLine, true);
#else
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_CommandLine(registry, m_commandLine, false);
#endif
// Update the Runtime file paths in case the "{BootstrapSettingsRootKey}/assets" key was overriden by a setting registry
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(registry);

@ -28,8 +28,10 @@ namespace AzToolsFramework
//////////////////////////////////////////////////////////////////////////
//! Triggered when the editor focus is changed to a different entity.
//! @param entityId The entity the focus has been moved to.
virtual void OnEditorFocusChanged(AZ::EntityId entityId) = 0;
//! @param previousFocusEntityId The entity the focus has been moved from.
//! @param newFocusEntityId The entity the focus has been moved to.
virtual void OnEditorFocusChanged(
[[maybe_unused]] AZ::EntityId previousFocusEntityId, [[maybe_unused]] AZ::EntityId newFocusEntityId) {}
protected:
~FocusModeNotifications() = default;

@ -71,8 +71,9 @@ namespace AzToolsFramework
return;
}
AZ::EntityId previousFocusEntityId = m_focusRoot;
m_focusRoot = entityId;
FocusModeNotificationBus::Broadcast(&FocusModeNotifications::OnEditorFocusChanged, m_focusRoot);
FocusModeNotificationBus::Broadcast(&FocusModeNotifications::OnEditorFocusChanged, previousFocusEntityId, m_focusRoot);
if (auto tracker = AZ::Interface<ViewportEditorModeTrackerInterface>::Get();
tracker != nullptr)

@ -8,12 +8,14 @@
#include <AzToolsFramework/Prefab/PrefabFocusHandler.h>
#include <AzToolsFramework/Commands/SelectionCommand.h>
#include <AzToolsFramework/ContainerEntity/ContainerEntityInterface.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusNotificationBus.h>
#include <AzToolsFramework/Prefab/PrefabFocusUndo.h>
namespace AzToolsFramework::Prefab
{
@ -28,10 +30,12 @@ namespace AzToolsFramework::Prefab
EditorEntityContextNotificationBus::Handler::BusConnect();
AZ::Interface<PrefabFocusInterface>::Register(this);
AZ::Interface<PrefabFocusPublicInterface>::Register(this);
}
PrefabFocusHandler::~PrefabFocusHandler()
{
AZ::Interface<PrefabFocusPublicInterface>::Unregister(this);
AZ::Interface<PrefabFocusInterface>::Unregister(this);
EditorEntityContextNotificationBus::Handler::BusDisconnect();
}
@ -61,6 +65,44 @@ namespace AzToolsFramework::Prefab
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnOwningPrefab(AZ::EntityId entityId)
{
// Initialize Undo Batch object
ScopedUndoBatch undoBatch("Edit Prefab");
// Clear selection
{
const EntityIdList selectedEntities = EntityIdList{};
auto selectionUndo = aznew SelectionCommand(selectedEntities, "Clear Selection");
selectionUndo->SetParent(undoBatch.GetUndoBatch());
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, selectedEntities);
}
// Edit Prefab
{
auto editUndo = aznew PrefabFocusUndo("Edit Prefab");
editUndo->Capture(entityId);
editUndo->SetParent(undoBatch.GetUndoBatch());
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, editUndo);
}
return AZ::Success();
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnPathIndex([[maybe_unused]] AzFramework::EntityContextId entityContextId, int index)
{
if (index < 0 || index >= m_instanceFocusVector.size())
{
return AZ::Failure(AZStd::string("Prefab Focus Handler: Invalid index on FocusOnPathIndex."));
}
InstanceOptionalReference focusedInstance = m_instanceFocusVector[index];
FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId());
return AZ::Success();
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId)
{
InstanceOptionalReference focusedInstance;
@ -85,18 +127,6 @@ namespace AzToolsFramework::Prefab
return FocusOnPrefabInstance(focusedInstance);
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnPathIndex([[maybe_unused]] AzFramework::EntityContextId entityContextId, int index)
{
if (index < 0 || index >= m_instanceFocusVector.size())
{
return AZ::Failure(AZStd::string("Prefab Focus Handler: Invalid index on FocusOnPathIndex."));
}
InstanceOptionalReference focusedInstance = m_instanceFocusVector[index];
return FocusOnPrefabInstance(focusedInstance);
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnPrefabInstance(InstanceOptionalReference focusedInstance)
{
if (!focusedInstance.has_value())
@ -122,17 +152,10 @@ namespace AzToolsFramework::Prefab
if (focusedInstance->get().GetParentInstance() != AZStd::nullopt)
{
containerEntityId = focusedInstance->get().GetContainerEntityId();
// Select the container entity
AzToolsFramework::SelectEntity(containerEntityId);
}
else
{
containerEntityId = AZ::EntityId();
// Clear the selection
AzToolsFramework::SelectEntities({});
}
// Focus on the descendants of the container entity
@ -161,6 +184,17 @@ namespace AzToolsFramework::Prefab
return m_focusedInstance;
}
AZ::EntityId PrefabFocusHandler::GetFocusedPrefabContainerEntityId([[maybe_unused]] AzFramework::EntityContextId entityContextId) const
{
if (!m_focusedInstance.has_value())
{
// PrefabFocusHandler has not been initialized yet.
return AZ::EntityId();
}
return m_focusedInstance->get().GetContainerEntityId();
}
bool PrefabFocusHandler::IsOwningPrefabBeingFocused(AZ::EntityId entityId) const
{
if (!m_focusedInstance.has_value())
@ -200,7 +234,7 @@ namespace AzToolsFramework::Prefab
m_instanceFocusVector.clear();
// Focus on the root prefab (AZ::EntityId() will default to it)
FocusOnOwningPrefab(AZ::EntityId());
FocusOnPrefabInstanceOwningEntityId(AZ::EntityId());
}
void PrefabFocusHandler::RefreshInstanceFocusList()

@ -13,6 +13,7 @@
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/FocusMode/FocusModeInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
namespace AzToolsFramework
@ -28,6 +29,7 @@ namespace AzToolsFramework::Prefab
//! Handles Prefab Focus mode, determining which prefab file entity changes will target.
class PrefabFocusHandler final
: private PrefabFocusInterface
, private PrefabFocusPublicInterface
, private EditorEntityContextNotificationBus::Handler
{
public:
@ -39,10 +41,14 @@ namespace AzToolsFramework::Prefab
void Initialize();
// PrefabFocusInterface overrides ...
PrefabFocusOperationResult FocusOnOwningPrefab(AZ::EntityId entityId) override;
PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) override;
PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) override;
TemplateId GetFocusedPrefabTemplateId(AzFramework::EntityContextId entityContextId) const override;
InstanceOptionalReference GetFocusedPrefabInstance(AzFramework::EntityContextId entityContextId) const override;
// PrefabFocusPublicInterface overrides ...
PrefabFocusOperationResult FocusOnOwningPrefab(AZ::EntityId entityId) override;
PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) override;
AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const override;
bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const override;
const AZ::IO::Path& GetPrefabFocusPath(AzFramework::EntityContextId entityContextId) const override;
const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override;

@ -20,7 +20,7 @@ namespace AzToolsFramework::Prefab
{
using PrefabFocusOperationResult = AZ::Outcome<void, AZStd::string>;
//! Interface to handle operations related to the Prefab Focus system.
//! Interface to handle internal operations related to the Prefab Focus system.
class PrefabFocusInterface
{
public:
@ -28,29 +28,13 @@ namespace AzToolsFramework::Prefab
//! Set the focused prefab instance to the owning instance of the entityId provided.
//! @param entityId The entityId of the entity whose owning instance we want the prefab system to focus on.
virtual PrefabFocusOperationResult FocusOnOwningPrefab(AZ::EntityId entityId) = 0;
//! Set the focused prefab instance to the instance at position index of the current path.
//! @param index The index of the instance in the current path that we want the prefab system to focus on.
virtual PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) = 0;
virtual PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) = 0;
//! Returns the template id of the instance the prefab system is focusing on.
virtual TemplateId GetFocusedPrefabTemplateId(AzFramework::EntityContextId entityContextId) const = 0;
//! Returns a reference to the instance the prefab system is focusing on.
virtual InstanceOptionalReference GetFocusedPrefabInstance(AzFramework::EntityContextId entityContextId) const = 0;
//! Returns whether the entity belongs to the instance that is being focused on, or one of its descendants.
//! @param entityId The entityId of the queried entity.
//! @return true if the entity belongs to the focused instance or one of its descendants, false otherwise.
virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0;
//! Returns the path from the root instance to the currently focused instance.
//! @return A path composed from the names of the container entities for the instance path.
virtual const AZ::IO::Path& GetPrefabFocusPath(AzFramework::EntityContextId entityContextId) const = 0;
//! Returns the size of the path to the currently focused instance.
virtual const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const = 0;
};
} // namespace AzToolsFramework::Prefab

@ -0,0 +1,53 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Interface/Interface.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Entity/EntityContext.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
namespace AzToolsFramework::Prefab
{
using PrefabFocusOperationResult = AZ::Outcome<void, AZStd::string>;
//! Public Interface for external systems to utilize the Prefab Focus system.
class PrefabFocusPublicInterface
{
public:
AZ_RTTI(PrefabFocusPublicInterface, "{53EE1D18-A41F-4DB1-9B73-9448F425722E}");
//! Set the focused prefab instance to the owning instance of the entityId provided. Supports undo/redo.
//! @param entityId The entityId of the entity whose owning instance we want the prefab system to focus on.
virtual PrefabFocusOperationResult FocusOnOwningPrefab(AZ::EntityId entityId) = 0;
//! Set the focused prefab instance to the instance at position index of the current path. Supports undo/redo.
//! @param index The index of the instance in the current path that we want the prefab system to focus on.
virtual PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) = 0;
//! Returns the entity id of the container entity for the instance the prefab system is focusing on.
virtual AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const = 0;
//! Returns whether the entity belongs to the instance that is being focused on, or one of its descendants.
//! @param entityId The entityId of the queried entity.
//! @return true if the entity belongs to the focused instance or one of its descendants, false otherwise.
virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0;
//! Returns the path from the root instance to the currently focused instance.
//! @return A path composed from the names of the container entities for the instance path.
virtual const AZ::IO::Path& GetPrefabFocusPath(AzFramework::EntityContextId entityContextId) const = 0;
//! Returns the size of the path to the currently focused instance.
virtual const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const = 0;
};
} // namespace AzToolsFramework::Prefab

@ -0,0 +1,52 @@
/*
* 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 <AzToolsFramework/Prefab/PrefabFocusUndo.h>
#include <AzCore/Interface/Interface.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
namespace AzToolsFramework::Prefab
{
PrefabFocusUndo::PrefabFocusUndo(const AZStd::string& undoOperationName)
: UndoSystem::URSequencePoint(undoOperationName)
{
m_prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
AZ_Assert(m_prefabFocusInterface, "PrefabFocusUndo - Failed to grab prefab focus interface");
m_prefabFocusPublicInterface = AZ::Interface<PrefabFocusPublicInterface>::Get();
AZ_Assert(m_prefabFocusPublicInterface, "PrefabFocusUndo - Failed to grab prefab focus public interface");
}
bool PrefabFocusUndo::Changed() const
{
return true;
}
void PrefabFocusUndo::Capture(AZ::EntityId entityId)
{
auto entityContextId = AzFramework::EntityContextId::CreateNull();
EditorEntityContextRequestBus::BroadcastResult(entityContextId, &EditorEntityContextRequests::GetEditorEntityContextId);
m_beforeEntityId = m_prefabFocusPublicInterface->GetFocusedPrefabContainerEntityId(entityContextId);
m_afterEntityId = entityId;
}
void PrefabFocusUndo::Undo()
{
m_prefabFocusInterface->FocusOnPrefabInstanceOwningEntityId(m_beforeEntityId);
}
void PrefabFocusUndo::Redo()
{
m_prefabFocusInterface->FocusOnPrefabInstanceOwningEntityId(m_afterEntityId);
}
} // namespace AzToolsFramework::Prefab

@ -0,0 +1,39 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Component/EntityId.h>
#include <AzToolsFramework/Undo/UndoSystem.h>
namespace AzToolsFramework::Prefab
{
class PrefabFocusInterface;
class PrefabFocusPublicInterface;
//! Undo node for prefab focus change operations.
class PrefabFocusUndo
: public UndoSystem::URSequencePoint
{
public:
explicit PrefabFocusUndo(const AZStd::string& undoOperationName);
bool Changed() const override;
void Capture(AZ::EntityId entityId);
void Undo() override;
void Redo() override;
protected:
PrefabFocusInterface* m_prefabFocusInterface = nullptr;
PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr;
AZ::EntityId m_beforeEntityId;
AZ::EntityId m_afterEntityId;
};
} // namespace AzToolsFramework::Prefab

@ -257,9 +257,10 @@ namespace AzToolsFramework
// Select Container Entity
{
auto selectionUndo = aznew SelectionCommand({ containerEntityId }, "Select Prefab Container Entity");
const EntityIdList selectedEntities = EntityIdList{ containerEntityId };
auto selectionUndo = aznew SelectionCommand(selectedEntities, "Select Prefab Container Entity");
selectionUndo->SetParent(undoBatch.GetUndoBatch());
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, selectionUndo);
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, selectedEntities);
}
}
@ -1097,7 +1098,7 @@ namespace AzToolsFramework
// Select the duplicated entities/instances
auto selectionUndo = aznew SelectionCommand(duplicatedEntityAndInstanceIds, "Select Duplicated Entities/Instances");
selectionUndo->SetParent(undoBatch.GetUndoBatch());
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, selectionUndo);
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds);
}
return AZ::Success();

@ -313,7 +313,8 @@ namespace AzToolsFramework
StyledTreeView::StartCustomDrag(indexListSorted, supportedActions);
}
void EntityOutlinerTreeView::OnEditorFocusChanged([[maybe_unused]] AZ::EntityId entityId)
void EntityOutlinerTreeView::OnEditorFocusChanged(
[[maybe_unused]] AZ::EntityId previousFocusEntityId, [[maybe_unused]] AZ::EntityId newFocusEntityId)
{
viewport()->repaint();
}

@ -64,7 +64,7 @@ namespace AzToolsFramework
void leaveEvent(QEvent* event) override;
// FocusModeNotificationBus overrides ...
void OnEditorFocusChanged(AZ::EntityId entityId) override;
void OnEditorFocusChanged(AZ::EntityId previousFocusEntityId, AZ::EntityId newFocusEntityId) override;
//! Renders the left side of the item: appropriate background, branch lines, icons.
void drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const override;

@ -324,7 +324,8 @@ namespace AzToolsFramework
// Currently, the first behavior is implemented.
void EntityOutlinerWidget::OnSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
{
if (m_selectionChangeInProgress || !m_enableSelectionUpdates)
if (m_selectionChangeInProgress || !m_enableSelectionUpdates
|| (selected.empty() && deselected.empty()))
{
return;
}
@ -552,6 +553,13 @@ namespace AzToolsFramework
return;
}
// Do not display the context menu if the item under the mouse cursor is not selectable.
if (const QModelIndex& index = m_gui->m_objectTree->indexAt(pos); index.isValid()
&& (index.flags() & Qt::ItemIsSelectable) == 0)
{
return;
}
QMenu* contextMenu = new QMenu(this);
// Populate global context menu.

@ -24,11 +24,11 @@
#include <AzToolsFramework/AssetBrowser/AssetSelectionModel.h>
#include <AzToolsFramework/AssetBrowser/Entries/SourceAssetBrowserEntry.h>
#include <AzToolsFramework/ContainerEntity/ContainerEntityInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabLoaderInterface.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/ToolsComponents/EditorLayerComponentBus.h>
#include <AzToolsFramework/UI/EditorEntityUi/EditorEntityUiInterface.h>
#include <AzToolsFramework/UI/Prefab/PrefabIntegrationInterface.h>
@ -39,7 +39,6 @@
#include <AzQtComponents/Components/StyleManager.h>
#include <AzQtComponents/Components/Widgets/CardHeader.h>
#include <QApplication>
#include <QCheckBox>
#include <QDialog>
@ -56,14 +55,13 @@
#include <QVBoxLayout>
#include <QWidget>
namespace AzToolsFramework
{
namespace Prefab
{
ContainerEntityInterface* PrefabIntegrationManager::s_containerEntityInterface = nullptr;
EditorEntityUiInterface* PrefabIntegrationManager::s_editorEntityUiInterface = nullptr;
PrefabFocusInterface* PrefabIntegrationManager::s_prefabFocusInterface = nullptr;
PrefabFocusPublicInterface* PrefabIntegrationManager::s_prefabFocusPublicInterface = nullptr;
PrefabLoaderInterface* PrefabIntegrationManager::s_prefabLoaderInterface = nullptr;
PrefabPublicInterface* PrefabIntegrationManager::s_prefabPublicInterface = nullptr;
PrefabSystemComponentInterface* PrefabIntegrationManager::s_prefabSystemComponentInterface = nullptr;
@ -129,10 +127,10 @@ namespace AzToolsFramework
return;
}
s_prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
if (s_prefabFocusInterface == nullptr)
s_prefabFocusPublicInterface = AZ::Interface<PrefabFocusPublicInterface>::Get();
if (s_prefabFocusPublicInterface == nullptr)
{
AZ_Assert(false, "Prefab - could not get PrefabFocusInterface on PrefabIntegrationManager construction.");
AZ_Assert(false, "Prefab - could not get PrefabFocusPublicInterface on PrefabIntegrationManager construction.");
return;
}
@ -247,12 +245,8 @@ namespace AzToolsFramework
if (s_prefabPublicInterface->IsInstanceContainerEntity(selectedEntity))
{
// Edit Prefab
if (prefabWipFeaturesEnabled)
if (prefabWipFeaturesEnabled && !s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity))
{
bool beingEdited = s_prefabFocusInterface->IsOwningPrefabBeingFocused(selectedEntity);
if (!beingEdited)
{
QAction* editAction = menu->addAction(QObject::tr("Edit Prefab"));
editAction->setToolTip(QObject::tr("Edit the prefab in focus mode."));
@ -261,7 +255,6 @@ namespace AzToolsFramework
});
itemWasShown = true;
}
}
// Save Prefab
@ -317,7 +310,7 @@ namespace AzToolsFramework
void PrefabIntegrationManager::OnEscape()
{
s_prefabFocusInterface->FocusOnOwningPrefab(AZ::EntityId());
s_prefabFocusPublicInterface->FocusOnOwningPrefab(AZ::EntityId());
}
void PrefabIntegrationManager::HandleSourceFileType(AZStd::string_view sourceFilePath, AZ::EntityId parentId, AZ::Vector3 position) const
@ -490,7 +483,7 @@ namespace AzToolsFramework
void PrefabIntegrationManager::ContextMenu_EditPrefab(AZ::EntityId containerEntity)
{
s_prefabFocusInterface->FocusOnOwningPrefab(containerEntity);
s_prefabFocusPublicInterface->FocusOnOwningPrefab(containerEntity);
}
void PrefabIntegrationManager::ContextMenu_SavePrefab(AZ::EntityId containerEntity)

@ -30,7 +30,7 @@ namespace AzToolsFramework
namespace Prefab
{
class PrefabFocusInterface;
class PrefabFocusPublicInterface;
class PrefabLoaderInterface;
//! Structure for saving/retrieving user settings related to prefab workflows.
@ -144,7 +144,7 @@ namespace AzToolsFramework
static ContainerEntityInterface* s_containerEntityInterface;
static EditorEntityUiInterface* s_editorEntityUiInterface;
static PrefabFocusInterface* s_prefabFocusInterface;
static PrefabFocusPublicInterface* s_prefabFocusPublicInterface;
static PrefabLoaderInterface* s_prefabLoaderInterface;
static PrefabPublicInterface* s_prefabPublicInterface;
static PrefabSystemComponentInterface* s_prefabSystemComponentInterface;

@ -10,7 +10,7 @@
#include <AzFramework/API/ApplicationAPI.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
#include <AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx>
@ -35,10 +35,10 @@ namespace AzToolsFramework
return;
}
m_prefabFocusInterface = AZ::Interface<Prefab::PrefabFocusInterface>::Get();
if (m_prefabFocusInterface == nullptr)
m_prefabFocusPublicInterface = AZ::Interface<Prefab::PrefabFocusPublicInterface>::Get();
if (m_prefabFocusPublicInterface == nullptr)
{
AZ_Assert(false, "PrefabUiHandler - could not get PrefabFocusInterface on PrefabUiHandler construction.");
AZ_Assert(false, "PrefabUiHandler - could not get PrefabFocusPublicInterface on PrefabUiHandler construction.");
return;
}
}
@ -83,7 +83,7 @@ namespace AzToolsFramework
QIcon PrefabUiHandler::GenerateItemIcon(AZ::EntityId entityId) const
{
if (m_prefabFocusInterface->IsOwningPrefabBeingFocused(entityId))
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
return QIcon(m_prefabEditIconPath);
}
@ -105,7 +105,7 @@ namespace AzToolsFramework
const bool hasVisibleChildren = index.data(EntityOutlinerListModel::ExpandedRole).value<bool>() && index.model()->hasChildren(index);
QColor backgroundColor = m_prefabCapsuleColor;
if (m_prefabFocusInterface->IsOwningPrefabBeingFocused(entityId))
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
backgroundColor = m_prefabCapsuleEditColor;
}
@ -191,7 +191,7 @@ namespace AzToolsFramework
const bool isLastColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnLockToggle;
QColor borderColor = m_prefabCapsuleColor;
if (m_prefabFocusInterface->IsOwningPrefabBeingFocused(entityId))
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
borderColor = m_prefabCapsuleEditColor;
}
@ -329,7 +329,7 @@ namespace AzToolsFramework
if (prefabWipFeaturesEnabled)
{
// Focus on this prefab
m_prefabFocusInterface->FocusOnOwningPrefab(entityId);
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
}
}
}

@ -15,7 +15,7 @@ namespace AzToolsFramework
namespace Prefab
{
class PrefabFocusInterface;
class PrefabFocusPublicInterface;
class PrefabPublicInterface;
};
@ -39,7 +39,7 @@ namespace AzToolsFramework
void OnDoubleClick(AZ::EntityId entityId) const override;
private:
Prefab::PrefabFocusInterface* m_prefabFocusInterface = nullptr;
Prefab::PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr;
Prefab::PrefabPublicInterface* m_prefabPublicInterface = nullptr;
static bool IsLastVisibleChild(const QModelIndex& parent, const QModelIndex& child);

@ -8,7 +8,7 @@
#include <AzToolsFramework/UI/Prefab/PrefabViewportFocusPathHandler.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
namespace AzToolsFramework::Prefab
{
@ -31,8 +31,8 @@ namespace AzToolsFramework::Prefab
void PrefabViewportFocusPathHandler::Initialize(AzQtComponents::BreadCrumbs* breadcrumbsWidget, QToolButton* backButton)
{
// Get reference to the PrefabFocusInterface handler
m_prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
if (m_prefabFocusInterface == nullptr)
m_prefabFocusPublicInterface = AZ::Interface<PrefabFocusPublicInterface>::Get();
if (m_prefabFocusPublicInterface == nullptr)
{
AZ_Assert(false, "Prefab - could not get PrefabFocusInterface on PrefabViewportFocusPathHandler construction.");
return;
@ -46,7 +46,7 @@ namespace AzToolsFramework::Prefab
connect(m_breadcrumbsWidget, &AzQtComponents::BreadCrumbs::linkClicked, this,
[&](const QString&, int linkIndex)
{
m_prefabFocusInterface->FocusOnPathIndex(m_editorEntityContextId, linkIndex);
m_prefabFocusPublicInterface->FocusOnPathIndex(m_editorEntityContextId, linkIndex);
}
);
@ -54,9 +54,9 @@ namespace AzToolsFramework::Prefab
connect(m_backButton, &QToolButton::clicked, this,
[&]()
{
if (int length = m_prefabFocusInterface->GetPrefabFocusPathLength(m_editorEntityContextId); length > 1)
if (int length = m_prefabFocusPublicInterface->GetPrefabFocusPathLength(m_editorEntityContextId); length > 1)
{
m_prefabFocusInterface->FocusOnPathIndex(m_editorEntityContextId, length - 2);
m_prefabFocusPublicInterface->FocusOnPathIndex(m_editorEntityContextId, length - 2);
}
}
);
@ -65,7 +65,7 @@ namespace AzToolsFramework::Prefab
void PrefabViewportFocusPathHandler::OnPrefabFocusChanged()
{
// Push new Path
m_breadcrumbsWidget->pushPath(m_prefabFocusInterface->GetPrefabFocusPath(m_editorEntityContextId).c_str());
m_breadcrumbsWidget->pushPath(m_prefabFocusPublicInterface->GetPrefabFocusPath(m_editorEntityContextId).c_str());
}
} // namespace AzToolsFramework::Prefab

@ -19,7 +19,7 @@
namespace AzToolsFramework::Prefab
{
class PrefabFocusInterface;
class PrefabFocusPublicInterface;
class PrefabViewportFocusPathHandler
: public PrefabFocusNotificationBus::Handler
@ -40,6 +40,6 @@ namespace AzToolsFramework::Prefab
AzFramework::EntityContextId m_editorEntityContextId = AzFramework::EntityContextId::CreateNull();
PrefabFocusInterface* m_prefabFocusInterface = nullptr;
PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr;
};
} // namespace AzToolsFramework::Prefab

@ -38,7 +38,7 @@ namespace AzToolsFramework
static bool UnsignedToolTip(QWidget* widget, QString& toolTipString);
};
//! Base class for integer widget handlers to provide functionality independant
//! Base class for integer widget handlers to provide functionality independent
//! of widget type.
//! @tparam ValueType The integer primitive type of the handler.
//! @tparam PropertyControl The widget type of the handler.
@ -167,8 +167,7 @@ namespace AzToolsFramework
PropertyControl* newCtrl = aznew PropertyControl(pParent);
this->connect(newCtrl, &PropertyControl::valueChanged, this, [newCtrl]()
{
EBUS_EVENT(PropertyEditorGUIMessages::Bus, RequestWrite, newCtrl);
AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast(&PropertyEditorGUIMessages::Bus::Handler::RequestWrite, newCtrl);
AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast(&PropertyEditorGUIMessages::Bus::Events::RequestWrite, newCtrl);
});
// note: Qt automatically disconnects objects from each other when either end is destroyed, no need to worry about delete.

@ -98,11 +98,6 @@ namespace AzToolsFramework
QWidget* IntSpinBoxHandler<ValueType>::CreateGUI(QWidget* parent)
{
PropertyIntSpinCtrl* newCtrl = static_cast<PropertyIntSpinCtrl*>(BaseHandler::CreateGUI(parent));
this->connect(newCtrl, &PropertyIntSpinCtrl::valueChanged, [newCtrl]()
{
AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast(&PropertyEditorGUIMessages::Bus::Handler::RequestWrite, newCtrl);
});
return newCtrl;
}

@ -407,7 +407,7 @@ namespace AzToolsFramework
const AzFramework::CameraState cameraState = GetCameraState(viewportId);
for (size_t entityCacheIndex = 0; entityCacheIndex < entityDataCache.VisibleEntityDataCount(); ++entityCacheIndex)
{
if (entityDataCache.IsVisibleEntityLocked(entityCacheIndex) || !entityDataCache.IsVisibleEntityVisible(entityCacheIndex))
if (!entityDataCache.IsVisibleEntitySelectableInViewport(entityCacheIndex))
{
continue;
}
@ -1113,7 +1113,7 @@ namespace AzToolsFramework
});
m_boxSelect.InstallLeftMouseUp(
[this, entityBoxSelectData]()
[this, entityBoxSelectData]
{
entityBoxSelectData->m_boxSelectSelectionCommand->UpdateSelection(EntityIdVectorFromContainer(m_selectedEntityIds));
@ -2171,7 +2171,7 @@ namespace AzToolsFramework
// lock selection
AddAction(
m_actions, { QKeySequence(Qt::Key_L) }, LockSelection, LockSelectionTitle, LockSelectionDesc,
[lockUnlock]()
[lockUnlock]
{
lockUnlock(true);
});
@ -2179,7 +2179,7 @@ namespace AzToolsFramework
// unlock selection
AddAction(
m_actions, { QKeySequence(Qt::CTRL + Qt::Key_L) }, UnlockSelection, LockSelectionTitle, LockSelectionDesc,
[lockUnlock]()
[lockUnlock]
{
lockUnlock(false);
});
@ -2209,7 +2209,7 @@ namespace AzToolsFramework
// hide selection
AddAction(
m_actions, { QKeySequence(Qt::Key_H) }, HideSelection, HideSelectionTitle, HideSelectionDesc,
[showHide]()
[showHide]
{
showHide(false);
});
@ -2217,7 +2217,7 @@ namespace AzToolsFramework
// show selection
AddAction(
m_actions, { QKeySequence(Qt::CTRL + Qt::Key_H) }, ShowSelection, HideSelectionTitle, HideSelectionDesc,
[showHide]()
[showHide]
{
showHide(true);
});
@ -2225,7 +2225,7 @@ namespace AzToolsFramework
// unlock all entities in the level/scene
AddAction(
m_actions, { QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_L) }, UnlockAll, UnlockAllTitle, UnlockAllDesc,
[]()
[]
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
@ -2242,14 +2242,14 @@ namespace AzToolsFramework
// show all entities in the level/scene
AddAction(
m_actions, { QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_H) }, ShowAll, ShowAllTitle, ShowAllDesc,
[]()
[]
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
ScopedUndoBatch undoBatch(ShowAllEntitiesUndoRedoDesc);
EnumerateEditorEntities(
[](AZ::EntityId entityId)
[](const AZ::EntityId entityId)
{
ScopedUndoBatch::MarkEntityDirty(entityId);
SetEntityVisibility(entityId, true);
@ -2259,7 +2259,7 @@ namespace AzToolsFramework
// select all entities in the level/scene
AddAction(
m_actions, { QKeySequence(Qt::CTRL + Qt::Key_A) }, SelectAll, SelectAllTitle, SelectAllDesc,
[this]()
[this]
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
@ -2299,7 +2299,7 @@ namespace AzToolsFramework
// invert current selection
AddAction(
m_actions, { QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_I) }, InvertSelect, InvertSelectionTitle, InvertSelectionDesc,
[this]()
[this]
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
@ -2346,17 +2346,10 @@ namespace AzToolsFramework
// duplicate selection
AddAction(
m_actions, { QKeySequence(Qt::CTRL + Qt::Key_D) }, DuplicateSelect, DuplicateTitle, DuplicateDesc,
[]()
[]
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
// Clear Widget selection - Prevents issues caused by cloning entities while a property in the Reflected Property Editor
// is being edited.
if (QApplication::focusWidget())
{
QApplication::focusWidget()->clearFocus();
}
ScopedUndoBatch undoBatch(DuplicateUndoRedoDesc);
auto selectionCommand = AZStd::make_unique<SelectionCommand>(EntityIdList(), DuplicateUndoRedoDesc);
selectionCommand->SetParent(undoBatch.GetUndoBatch());
@ -2371,7 +2364,7 @@ namespace AzToolsFramework
// delete selection
AddAction(
m_actions, { QKeySequence(Qt::Key_Delete) }, DeleteSelect, DeleteTitle, DeleteDesc,
[this]()
[this]
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
@ -2388,21 +2381,21 @@ namespace AzToolsFramework
AddAction(
m_actions, { QKeySequence(Qt::Key_Space) }, EditEscaspe, "", "",
[this]()
[this]
{
DeselectEntities();
});
AddAction(
m_actions, { QKeySequence(Qt::Key_P) }, EditPivot, TogglePivotTitleEditMenu, TogglePivotDesc,
[this]()
[this]
{
ToggleCenterPivotSelection();
});
AddAction(
m_actions, { QKeySequence(Qt::Key_R) }, EditReset, ResetEntityTransformTitle, ResetEntityTransformDesc,
[this]()
[this]
{
switch (m_mode)
{
@ -2427,7 +2420,7 @@ namespace AzToolsFramework
AddAction(
m_actions, { QKeySequence(Qt::Key_U) }, ViewportUiVisible, "Toggle Viewport UI", "Hide/Show Viewport UI",
[this]()
[this]
{
SetAllViewportUiVisible(!m_viewportUiVisible);
});
@ -3236,7 +3229,7 @@ namespace AzToolsFramework
QAction* action = menu->addAction(QObject::tr(TogglePivotTitleRightClick));
QObject::connect(
action, &QAction::triggered, action,
[this]()
[this]
{
ToggleCenterPivotSelection();
});

@ -9,7 +9,9 @@
#include "EditorVisibleEntityDataCache.h"
#include <AzCore/std/sort.h>
#include <AzToolsFramework/ContainerEntity/ContainerEntityInterface.h>
#include <AzToolsFramework/Entity/EditorEntityModel.h>
#include <AzToolsFramework/FocusMode/FocusModeInterface.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <Entity/EditorEntityHelpers.h>
@ -21,13 +23,23 @@ namespace AzToolsFramework
using ComponentEntityAccentType = Components::EditorSelectionAccentSystemComponent::ComponentEntityAccentType;
EntityData() = default;
EntityData(AZ::EntityId entityId, const AZ::Transform& worldFromLocal, bool locked, bool visible, bool selected, bool iconHidden);
EntityData(
AZ::EntityId entityId,
const AZ::Transform& worldFromLocal,
bool locked,
bool visible,
bool inFocus,
bool descendantOfClosedContainer,
bool selected,
bool iconHidden);
AZ::Transform m_worldFromLocal;
AZ::EntityId m_entityId;
ComponentEntityAccentType m_accent = ComponentEntityAccentType::None;
bool m_locked = false;
bool m_visible = true;
bool m_inFocus = true;
bool m_descendantOfClosedContainer = false;
bool m_selected = false;
bool m_iconHidden = false;
};
@ -57,12 +69,16 @@ namespace AzToolsFramework
const AZ::Transform& worldFromLocal,
const bool locked,
const bool visible,
const bool inFocus,
const bool descendantOfClosedContainer,
const bool selected,
const bool iconHidden)
: m_worldFromLocal(worldFromLocal)
, m_entityId(entityId)
, m_locked(locked)
, m_visible(visible)
, m_inFocus(inFocus)
, m_descendantOfClosedContainer(descendantOfClosedContainer)
, m_selected(selected)
, m_iconHidden(iconHidden)
{
@ -106,6 +122,18 @@ namespace AzToolsFramework
bool locked = false;
EditorEntityInfoRequestBus::EventResult(locked, entityId, &EditorEntityInfoRequestBus::Events::IsLocked);
bool inFocus = false;
if (auto focusModeInterface = AZ::Interface<FocusModeInterface>::Get())
{
inFocus = focusModeInterface->IsInFocusSubTree(entityId);
}
bool descendantOfClosedContainer = false;
if (ContainerEntityInterface* containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get())
{
descendantOfClosedContainer = containerEntityInterface->IsUnderClosedContainerEntity(entityId);
}
bool iconHidden = false;
EditorEntityIconComponentRequestBus::EventResult(
iconHidden, entityId, &EditorEntityIconComponentRequests::IsEntityIconHiddenInViewport);
@ -113,7 +141,7 @@ namespace AzToolsFramework
AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity();
AZ::TransformBus::EventResult(worldFromLocal, entityId, &AZ::TransformBus::Events::GetWorldTM);
return { entityId, worldFromLocal, locked, visible, IsSelected(entityId), iconHidden };
return { entityId, worldFromLocal, locked, visible, inFocus, descendantOfClosedContainer, IsSelected(entityId), iconHidden };
}
EditorVisibleEntityDataCache::EditorVisibleEntityDataCache()
@ -126,10 +154,17 @@ namespace AzToolsFramework
EntitySelectionEvents::Bus::Router::BusRouterConnect();
EditorEntityIconComponentNotificationBus::Router::BusRouterConnect();
ToolsApplicationNotificationBus::Handler::BusConnect();
AzFramework::EntityContextId editorEntityContextId = AzToolsFramework::GetEntityContextId();
ContainerEntityNotificationBus::Handler::BusConnect(editorEntityContextId);
FocusModeNotificationBus::Handler::BusConnect(editorEntityContextId);
}
EditorVisibleEntityDataCache::~EditorVisibleEntityDataCache()
{
FocusModeNotificationBus::Handler::BusDisconnect();
ContainerEntityNotificationBus::Handler::BusDisconnect();
ToolsApplicationNotificationBus::Handler::BusDisconnect();
EditorEntityIconComponentNotificationBus::Router::BusRouterDisconnect();
EntitySelectionEvents::Bus::Router::BusRouterDisconnect();
@ -260,7 +295,10 @@ namespace AzToolsFramework
bool EditorVisibleEntityDataCache::IsVisibleEntitySelectableInViewport(size_t index) const
{
return m_impl->m_visibleEntityDatas[index].m_visible && !m_impl->m_visibleEntityDatas[index].m_locked;
return m_impl->m_visibleEntityDatas[index].m_visible
&& !m_impl->m_visibleEntityDatas[index].m_locked
&& m_impl->m_visibleEntityDatas[index].m_inFocus
&& !m_impl->m_visibleEntityDatas[index].m_descendantOfClosedContainer;
}
AZStd::optional<size_t> EditorVisibleEntityDataCache::GetVisibleEntityIndexFromId(const AZ::EntityId entityId) const
@ -371,4 +409,72 @@ namespace AzToolsFramework
m_impl->m_visibleEntityDatas[entityIndex.value()].m_iconHidden = iconHidden;
}
}
void EditorVisibleEntityDataCache::OnContainerEntityStatusChanged(AZ::EntityId entityId, [[maybe_unused]] bool open)
{
// Get container descendants
AzToolsFramework::EntityIdList descendantIds;
AZ::TransformBus::EventResult(descendantIds, entityId, &AZ::TransformBus::Events::GetAllDescendants);
// Update cached values
if (auto containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get())
{
for (AZ::EntityId descendantId : descendantIds)
{
if (AZStd::optional<size_t> entityIndex = GetVisibleEntityIndexFromId(descendantId))
{
m_impl->m_visibleEntityDatas[entityIndex.value()].m_descendantOfClosedContainer =
containerEntityInterface->IsUnderClosedContainerEntity(descendantId);
}
}
}
}
void EditorVisibleEntityDataCache::OnEditorFocusChanged(AZ::EntityId previousFocusEntityId, AZ::EntityId newFocusEntityId)
{
if (previousFocusEntityId.IsValid() && newFocusEntityId.IsValid())
{
// Get previous focus root descendants
AzToolsFramework::EntityIdList previousDescendantIds;
AZ::TransformBus::EventResult(previousDescendantIds, previousFocusEntityId, &AZ::TransformBus::Events::GetAllDescendants);
// Get new focus root descendants
AzToolsFramework::EntityIdList newDescendantIds;
AZ::TransformBus::EventResult(newDescendantIds, newFocusEntityId, &AZ::TransformBus::Events::GetAllDescendants);
// Merge EntityId Lists to avoid refreshing values twice
AzToolsFramework::EntityIdSet descendantsSet;
descendantsSet.insert(previousFocusEntityId);
descendantsSet.insert(newFocusEntityId);
descendantsSet.insert(previousDescendantIds.begin(), previousDescendantIds.end());
descendantsSet.insert(newDescendantIds.begin(), newDescendantIds.end());
// Update cached values
if (auto focusModeInterface = AZ::Interface<FocusModeInterface>::Get())
{
for (const AZ::EntityId& descendantId : descendantsSet)
{
if (AZStd::optional<size_t> entityIndex = GetVisibleEntityIndexFromId(descendantId))
{
m_impl->m_visibleEntityDatas[entityIndex.value()].m_inFocus = focusModeInterface->IsInFocusSubTree(descendantId);
}
}
}
}
else
{
// If either focus was the invalid entity, refresh all entities.
if (auto focusModeInterface = AZ::Interface<FocusModeInterface>::Get())
{
for (size_t entityIndex = 0; entityIndex < m_impl->m_visibleEntityDatas.size(); ++entityIndex)
{
if (AZ::EntityId descendantId = GetVisibleEntityId(entityIndex); descendantId.IsValid())
{
m_impl->m_visibleEntityDatas[entityIndex].m_inFocus = focusModeInterface->IsInFocusSubTree(descendantId);
}
}
}
}
}
} // namespace AzToolsFramework

@ -11,6 +11,8 @@
#include <AzCore/Component/TransformBus.h>
#include <AzCore/std/optional.h>
#include <AzToolsFramework/API/ComponentEntitySelectionBus.h>
#include <AzToolsFramework/ContainerEntity/ContainerEntityNotificationBus.h>
#include <AzToolsFramework/FocusMode/FocusModeNotificationBus.h>
#include <AzToolsFramework/ToolsComponents/EditorEntityIconComponentBus.h>
#include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h>
#include <AzToolsFramework/ToolsComponents/EditorSelectionAccentSystemComponent.h>
@ -28,6 +30,8 @@ namespace AzToolsFramework
, private EntitySelectionEvents::Bus::Router
, private EditorEntityIconComponentNotificationBus::Router
, private ToolsApplicationNotificationBus::Handler
, private ContainerEntityNotificationBus::Handler
, private FocusModeNotificationBus::Handler
{
public:
EditorVisibleEntityDataCache();
@ -58,28 +62,34 @@ namespace AzToolsFramework
void AddEntityIds(const EntityIdList& entityIds);
private:
// ToolsApplicationNotificationBus
// ToolsApplicationNotificationBus overrides ...
void AfterUndoRedo() override;
// EditorEntityVisibilityNotificationBus
// EditorEntityVisibilityNotificationBus overrides ...
void OnEntityVisibilityChanged(bool visibility) override;
// EditorEntityLockComponentNotificationBus
// EditorEntityLockComponentNotificationBus overrides ...
void OnEntityLockChanged(bool locked) override;
// TransformNotificationBus
// TransformNotificationBus overrides ...
void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override;
// EditorComponentSelectionNotificationsBus
// EditorComponentSelectionNotificationsBus overrides ...
void OnAccentTypeChanged(EntityAccentType accent) override;
// EntitySelectionEvents::Bus
// EntitySelectionEvents::Bus overrides ...
void OnSelected() override;
void OnDeselected() override;
// EditorEntityIconComponentNotificationBus
// EditorEntityIconComponentNotificationBus overrides ...
void OnEntityIconChanged(const AZ::Data::AssetId& entityIconAssetId) override;
// ContainerEntityNotificationBus overrides ...
void OnContainerEntityStatusChanged(AZ::EntityId entityId, bool open) override;
// FocusModeNotificationBus overrides ...
void OnEditorFocusChanged(AZ::EntityId previousFocusEntityId, AZ::EntityId newFocusEntityId) override;
class EditorVisibleEntityDataCacheImpl;
AZStd::unique_ptr<EditorVisibleEntityDataCacheImpl> m_impl; //!< Internal representation of entity data cache.
};

@ -646,6 +646,9 @@ set(FILES
Prefab/PrefabFocusHandler.cpp
Prefab/PrefabFocusInterface.h
Prefab/PrefabFocusNotificationBus.h
Prefab/PrefabFocusPublicInterface.h
Prefab/PrefabFocusUndo.h
Prefab/PrefabFocusUndo.cpp
Prefab/PrefabIdTypes.h
Prefab/PrefabLoader.h
Prefab/PrefabLoader.cpp

@ -10,6 +10,7 @@
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <Prefab/PrefabTestFixture.h>
namespace UnitTest
@ -72,6 +73,9 @@ namespace UnitTest
m_prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
ASSERT_TRUE(m_prefabFocusInterface != nullptr);
m_prefabFocusPublicInterface = AZ::Interface<PrefabFocusPublicInterface>::Get();
ASSERT_TRUE(m_prefabFocusPublicInterface != nullptr);
AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(
m_editorEntityContextId, &AzToolsFramework::EditorEntityContextRequestBus::Events::GetEditorEntityContextId);
@ -91,6 +95,7 @@ namespace UnitTest
AZStd::unique_ptr<AzToolsFramework::Prefab::Instance> m_rootInstance;
PrefabFocusInterface* m_prefabFocusInterface = nullptr;
PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr;
AzFramework::EntityContextId m_editorEntityContextId = AzFramework::EntityContextId::CreateNull();
inline static const char* CityEntityName = "City";
@ -105,7 +110,7 @@ namespace UnitTest
{
// Verify FocusOnOwningPrefab works when passing the container entity of the root prefab.
{
m_prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap[CityEntityName]->GetContainerEntityId());
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_instanceMap[CityEntityName]->GetContainerEntityId());
EXPECT_EQ(
m_prefabFocusInterface->GetFocusedPrefabTemplateId(m_editorEntityContextId),
m_instanceMap[CityEntityName]->GetTemplateId());
@ -120,7 +125,7 @@ namespace UnitTest
{
// Verify FocusOnOwningPrefab works when passing a nested entity of the root prefab.
{
m_prefabFocusInterface->FocusOnOwningPrefab(m_entityMap[CityEntityName]->GetId());
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_entityMap[CityEntityName]->GetId());
EXPECT_EQ(
m_prefabFocusInterface->GetFocusedPrefabTemplateId(m_editorEntityContextId),
m_instanceMap[CityEntityName]->GetTemplateId());
@ -135,7 +140,7 @@ namespace UnitTest
{
// Verify FocusOnOwningPrefab works when passing the container entity of a nested prefab.
{
m_prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap[CarEntityName]->GetContainerEntityId());
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_instanceMap[CarEntityName]->GetContainerEntityId());
EXPECT_EQ(
m_prefabFocusInterface->GetFocusedPrefabTemplateId(m_editorEntityContextId), m_instanceMap[CarEntityName]->GetTemplateId());
@ -149,7 +154,7 @@ namespace UnitTest
{
// Verify FocusOnOwningPrefab works when passing a nested entity of the a nested prefab.
{
m_prefabFocusInterface->FocusOnOwningPrefab(m_entityMap[Passenger1EntityName]->GetId());
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_entityMap[Passenger1EntityName]->GetId());
EXPECT_EQ(
m_prefabFocusInterface->GetFocusedPrefabTemplateId(m_editorEntityContextId), m_instanceMap[CarEntityName]->GetTemplateId());
@ -169,7 +174,7 @@ namespace UnitTest
prefabEditorEntityOwnershipInterface->GetRootPrefabInstance();
EXPECT_TRUE(rootPrefabInstance.has_value());
m_prefabFocusInterface->FocusOnOwningPrefab(AZ::EntityId());
m_prefabFocusPublicInterface->FocusOnOwningPrefab(AZ::EntityId());
EXPECT_EQ(
m_prefabFocusInterface->GetFocusedPrefabTemplateId(m_editorEntityContextId), rootPrefabInstance->get().GetTemplateId());
@ -183,10 +188,10 @@ namespace UnitTest
{
// Verify IsOwningPrefabBeingFocused returns true for all entities in a focused prefab (container/nested)
{
m_prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap[CityEntityName]->GetContainerEntityId());
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_instanceMap[CityEntityName]->GetContainerEntityId());
EXPECT_TRUE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap[CityEntityName]->GetContainerEntityId()));
EXPECT_TRUE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap[CityEntityName]->GetId()));
EXPECT_TRUE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_instanceMap[CityEntityName]->GetContainerEntityId()));
EXPECT_TRUE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_entityMap[CityEntityName]->GetId()));
}
}
@ -194,13 +199,13 @@ namespace UnitTest
{
// Verify IsOwningPrefabBeingFocused returns false for all entities not in a focused prefab (ancestors/descendants)
{
m_prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap[StreetEntityName]->GetContainerEntityId());
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_instanceMap[StreetEntityName]->GetContainerEntityId());
EXPECT_TRUE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap[StreetEntityName]->GetContainerEntityId()));
EXPECT_FALSE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap[CityEntityName]->GetContainerEntityId()));
EXPECT_FALSE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap[CityEntityName]->GetId()));
EXPECT_FALSE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap[CarEntityName]->GetContainerEntityId()));
EXPECT_FALSE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap[Passenger1EntityName]->GetId()));
EXPECT_TRUE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_instanceMap[StreetEntityName]->GetContainerEntityId()));
EXPECT_FALSE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_instanceMap[CityEntityName]->GetContainerEntityId()));
EXPECT_FALSE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_entityMap[CityEntityName]->GetId()));
EXPECT_FALSE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_instanceMap[CarEntityName]->GetContainerEntityId()));
EXPECT_FALSE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_entityMap[Passenger1EntityName]->GetId()));
}
}
@ -208,12 +213,12 @@ namespace UnitTest
{
// Verify IsOwningPrefabBeingFocused returns false for all entities not in a focused prefab (siblings)
{
m_prefabFocusInterface->FocusOnOwningPrefab(m_instanceMap[SportsCarEntityName]->GetContainerEntityId());
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_instanceMap[SportsCarEntityName]->GetContainerEntityId());
EXPECT_TRUE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap[SportsCarEntityName]->GetContainerEntityId()));
EXPECT_TRUE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap[Passenger2EntityName]->GetId()));
EXPECT_FALSE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_instanceMap[CarEntityName]->GetContainerEntityId()));
EXPECT_FALSE(m_prefabFocusInterface->IsOwningPrefabBeingFocused(m_entityMap[Passenger1EntityName]->GetId()));
EXPECT_TRUE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_instanceMap[SportsCarEntityName]->GetContainerEntityId()));
EXPECT_TRUE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_entityMap[Passenger2EntityName]->GetId()));
EXPECT_FALSE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_instanceMap[CarEntityName]->GetContainerEntityId()));
EXPECT_FALSE(m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(m_entityMap[Passenger1EntityName]->GetId()));
}
}

@ -13,6 +13,7 @@
#include <AzCore/Math/Crc.h>
#include <AzCore/Math/Quaternion.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/allocator_stateless.h>
#include <Range.h>
#include <AnimKey.h>
@ -181,7 +182,7 @@ public:
private:
AnimParamType m_type;
AZStd::string m_name;
AZStd::basic_string<char, AZStd::char_traits<char>, AZStd::stateless_allocator> m_name;
};
namespace AZStd
@ -617,7 +618,7 @@ public:
, valueType(_valueType)
, flags(_flags) {};
AZStd::string name; // parameter name.
AZStd::basic_string<char, AZStd::char_traits<char>, AZStd::stateless_allocator> name; // parameter name.
CAnimParamType paramType; // parameter id.
AnimValueType valueType; // value type, defines type of track to use for animating this parameter.
ESupportedParamFlags flags; // combination of flags from ESupportedParamFlags.

@ -13,6 +13,7 @@
#include <unordered_map>
#include <unordered_set>
#include <AzCore/std/allocator_stateless.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/string/string.h>
@ -491,6 +492,13 @@ namespace stl
return type;
}
//! Specialization of string to const char cast.
template <>
inline const char* constchar_cast(const AZStd::basic_string<char, AZStd::char_traits<char>, AZStd::stateless_allocator>& type)
{
return type.c_str();
}
//! Specialization of string to const char cast.
template <>
inline const char* constchar_cast(const AZStd::string& type)

@ -27,7 +27,7 @@ ly_add_target(
3rdParty::expat
3rdParty::lz4
3rdParty::md5
3rdParty::tiff
3rdParty::TIFF
3rdParty::zstd
Legacy::CryCommon
Legacy::CrySystem.XMLBinary

@ -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)

@ -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));
}
}
}

@ -0,0 +1,3 @@
<svg width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7872 2.91865C11.4965 1.25029 10.0476 0 8.31169 0C6.91408 0 5.66005 0.823481 5.09382 2.08042C5.05787 2.07875 5.02234 2.07792 4.98701 2.07792C3.81195 2.07792 2.7921 2.8241 2.42743 3.92062C0.986389 4.39377 0 5.73881 0 7.27273C0 9.22057 1.58462 10.8052 3.53247 10.8052H12.0519C14.229 10.8052 16 9.03418 16 6.85714C16 4.59242 14.085 2.75595 11.7872 2.91865ZM8.31169 9.35065L5.61039 6.44156H7.27273V4.57143H9.35065V6.44156H11.013L8.31169 9.35065Z" fill="#E4E8EB"/>
</svg>

After

Width:  |  Height:  |  Size: 575 B

@ -34,9 +34,11 @@
<file>Warning.svg</file>
<file>Backgrounds/DefaultBackground.jpg</file>
<file>Backgrounds/FtueBackground.jpg</file>
<file>FeatureTagClose.svg</file>
<file>X.svg</file>
<file>Refresh.svg</file>
<file>Edit.svg</file>
<file>Delete.svg</file>
<file>Download.svg</file>
<file>in_progress.gif</file>
</qresource>
</RCC>

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 400 B

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64985a78205da45f4bb92b040c348d96fe7cd7277549c1f79c430469a0d3bab7
size 166393

@ -33,7 +33,7 @@ namespace O3DE::ProjectManager
m_closeButton = new QPushButton();
m_closeButton->setFlat(true);
m_closeButton->setIcon(QIcon(":/FeatureTagClose.svg"));
m_closeButton->setIcon(QIcon(":/X.svg"));
m_closeButton->setIconSize(QSize(12, 12));
m_closeButton->setStyleSheet("QPushButton { background-color: transparent; border: 0px }");
layout->addWidget(m_closeButton);

@ -8,6 +8,8 @@
#include "GemInfo.h"
#include <QObject>
namespace O3DE::ProjectManager
{
GemInfo::GemInfo(const QString& name, const QString& creator, const QString& summary, Platforms platforms, bool isAdded)
@ -29,17 +31,17 @@ namespace O3DE::ProjectManager
switch (platform)
{
case Android:
return "Android";
return QObject::tr("Android");
case iOS:
return "iOS";
return QObject::tr("iOS");
case Linux:
return "Linux";
return QObject::tr("Linux");
case macOS:
return "macOS";
return QObject::tr("macOS");
case Windows:
return "Windows";
return QObject::tr("Windows");
default:
return "<Unknown Platform>";
return QObject::tr("<Unknown Platform>");
}
}
@ -48,13 +50,13 @@ namespace O3DE::ProjectManager
switch (type)
{
case Asset:
return "Asset";
return QObject::tr("Asset");
case Code:
return "Code";
return QObject::tr("Code");
case Tool:
return "Tool";
return QObject::tr("Tool");
default:
return "<Unknown Type>";
return QObject::tr("<Unknown Type>");
}
}
@ -62,15 +64,33 @@ namespace O3DE::ProjectManager
{
switch (origin)
{
case Open3DEEngine:
return "Open 3D Engine";
case Open3DEngine:
return QObject::tr("Open 3D Engine");
case Local:
return "Local";
return QObject::tr("Local");
case Remote:
return QObject::tr("Remote");
default:
return "<Unknown Gem Origin>";
return QObject::tr("<Unknown Gem Origin>");
}
}
QString GemInfo::GetDownloadStatusString(DownloadStatus status)
{
switch (status)
{
case NotDownloaded:
return QObject::tr("Not Downloaded");
case Downloading:
return QObject::tr("Downloading");
case Downloaded:
return QObject::tr("Downloaded");
case UnknownDownloadStatus:
default:
return QObject::tr("<Unknown Download Status>");
}
};
bool GemInfo::IsPlatformSupported(Platform platform) const
{
return (m_platforms & platform);

@ -44,13 +44,23 @@ namespace O3DE::ProjectManager
enum GemOrigin
{
Open3DEEngine = 1 << 0,
Open3DEngine = 1 << 0,
Local = 1 << 1,
NumGemOrigins = 2
Remote = 1 << 2,
NumGemOrigins = 3
};
Q_DECLARE_FLAGS(GemOrigins, GemOrigin)
static QString GetGemOriginString(GemOrigin origin);
enum DownloadStatus
{
UnknownDownloadStatus = -1,
NotDownloaded,
Downloading,
Downloaded,
};
static QString GetDownloadStatusString(DownloadStatus status);
GemInfo() = default;
GemInfo(const QString& name, const QString& creator, const QString& summary, Platforms platforms, bool isAdded);
bool IsPlatformSupported(Platform platform) const;
@ -68,6 +78,7 @@ namespace O3DE::ProjectManager
QString m_summary = "No summary provided.";
Platforms m_platforms;
Types m_types; //! Asset and/or Code and/or Tool
DownloadStatus m_downloadStatus = UnknownDownloadStatus;
QStringList m_features;
QString m_requirement;
QString m_directoryLink;

@ -10,6 +10,7 @@
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <QEvent>
#include <QAbstractItemView>
#include <QPainter>
@ -20,6 +21,7 @@
#include <QTextDocument>
#include <QAbstractTextDocumentLayout>
#include <QDesktopServices>
#include <QMovie>
namespace O3DE::ProjectManager
{
@ -32,6 +34,11 @@ namespace O3DE::ProjectManager
AddPlatformIcon(GemInfo::Linux, ":/Linux.svg");
AddPlatformIcon(GemInfo::macOS, ":/macOS.svg");
AddPlatformIcon(GemInfo::Windows, ":/Windows.svg");
SetStatusIcon(m_notDownloadedPixmap, ":/Download.svg");
SetStatusIcon(m_unknownStatusPixmap, ":/X.svg");
m_downloadingMovie = new QMovie(":/in_progress.gif");
}
void GemItemDelegate::AddPlatformIcon(GemInfo::Platform platform, const QString& iconPath)
@ -41,6 +48,25 @@ namespace O3DE::ProjectManager
m_platformIcons.insert(platform, QIcon(iconPath).pixmap(static_cast<int>(static_cast<qreal>(s_platformIconSize) * aspectRatio), s_platformIconSize));
}
void GemItemDelegate::SetStatusIcon(QPixmap& m_iconPixmap, const QString& iconPath)
{
QPixmap pixmap(iconPath);
float aspectRatio = static_cast<float>(pixmap.width()) / pixmap.height();
int xScaler = s_statusIconSize;
int yScaler = s_statusIconSize;
if (aspectRatio > 1.0f)
{
yScaler = static_cast<int>(1.0f / aspectRatio * s_statusIconSize);
}
else if (aspectRatio < 1.0f)
{
xScaler = static_cast<int>(aspectRatio * s_statusIconSize);
}
m_iconPixmap = QPixmap(QIcon(iconPath).pixmap(xScaler, yScaler));
}
void GemItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const
{
if (!modelIndex.isValid())
@ -56,6 +82,8 @@ namespace O3DE::ProjectManager
QRect fullRect, itemRect, contentRect;
CalcRects(options, fullRect, itemRect, contentRect);
QRect buttonRect = CalcButtonRect(contentRect);
QFont standardFont(options.font);
standardFont.setPixelSize(static_cast<int>(s_fontSize));
QFontMetrics standardFontMetrics(standardFont);
@ -114,7 +142,8 @@ namespace O3DE::ProjectManager
const QRect summaryRect = CalcSummaryRect(contentRect, hasTags);
DrawText(summary, painter, summaryRect, standardFont);
DrawButton(painter, contentRect, modelIndex);
DrawDownloadStatusIcon(painter, contentRect, buttonRect, modelIndex);
DrawButton(painter, buttonRect, modelIndex);
DrawPlatformIcons(painter, contentRect, modelIndex);
DrawFeatureTags(painter, contentRect, featureTags, standardFont, summaryRect);
@ -270,7 +299,7 @@ namespace O3DE::ProjectManager
QRect GemItemDelegate::CalcButtonRect(const QRect& contentRect) const
{
const QPoint topLeft = QPoint(contentRect.right() - s_buttonWidth - s_itemMargins.right(), contentRect.top() + contentRect.height() / 2 - s_buttonHeight / 2);
const QPoint topLeft = QPoint(contentRect.right() - s_buttonWidth, contentRect.center().y() - s_buttonHeight / 2);
const QSize size = QSize(s_buttonWidth, s_buttonHeight);
return QRect(topLeft, size);
}
@ -378,10 +407,9 @@ namespace O3DE::ProjectManager
painter->restore();
}
void GemItemDelegate::DrawButton(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const
void GemItemDelegate::DrawButton(QPainter* painter, const QRect& buttonRect, const QModelIndex& modelIndex) const
{
painter->save();
const QRect buttonRect = CalcButtonRect(contentRect);
QPoint circleCenter;
if (GemModel::IsAdded(modelIndex))
@ -427,4 +455,45 @@ namespace O3DE::ProjectManager
return QString();
}
void GemItemDelegate::DrawDownloadStatusIcon(QPainter* painter, const QRect& contentRect, const QRect& buttonRect, const QModelIndex& modelIndex) const
{
const GemInfo::DownloadStatus downloadStatus = GemModel::GetDownloadStatus(modelIndex);
// Show no icon if gem is already downloaded
if (downloadStatus == GemInfo::DownloadStatus::Downloaded)
{
return;
}
QPixmap currentFrame;
const QPixmap* statusPixmap;
if (downloadStatus == GemInfo::DownloadStatus::Downloading)
{
if (m_downloadingMovie->state() != QMovie::Running)
{
m_downloadingMovie->start();
emit MovieStartedPlaying(m_downloadingMovie);
}
currentFrame = m_downloadingMovie->currentPixmap();
currentFrame = currentFrame.scaled(s_statusIconSize, s_statusIconSize);
statusPixmap = &currentFrame;
}
else if (downloadStatus == GemInfo::DownloadStatus::NotDownloaded)
{
statusPixmap = &m_notDownloadedPixmap;
}
else
{
statusPixmap = &m_unknownStatusPixmap;
}
QSize statusSize = statusPixmap->size();
painter->drawPixmap(
buttonRect.left() - s_statusButtonSpacing - statusSize.width(),
contentRect.center().y() - statusSize.height() / 2,
*statusPixmap);
}
} // namespace O3DE::ProjectManager

@ -49,13 +49,13 @@ namespace O3DE::ProjectManager
// Margin and borders
inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/16, /*top=*/8, /*right=*/16, /*bottom=*/8); // Item border distances
inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/20, /*top=*/12, /*right=*/15, /*bottom=*/12); // Distances of the elements within an item to the item borders
inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/20, /*top=*/12, /*right=*/20, /*bottom=*/12); // Distances of the elements within an item to the item borders
inline constexpr static int s_borderWidth = 4;
// Button
inline constexpr static int s_buttonWidth = 55;
inline constexpr static int s_buttonHeight = 18;
inline constexpr static int s_buttonBorderRadius = 9;
inline constexpr static int s_buttonWidth = 32;
inline constexpr static int s_buttonHeight = 16;
inline constexpr static int s_buttonBorderRadius = s_buttonHeight / 2;
inline constexpr static int s_buttonCircleRadius = s_buttonBorderRadius - 2;
inline constexpr static qreal s_buttonFontSize = 10.0;
@ -65,6 +65,9 @@ namespace O3DE::ProjectManager
inline constexpr static int s_featureTagBorderMarginY = 3;
inline constexpr static int s_featureTagSpacing = 7;
signals:
void MovieStartedPlaying(const QMovie* playingMovie) const;
protected:
bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) override;
bool helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& option, const QModelIndex& index) override;
@ -74,9 +77,10 @@ namespace O3DE::ProjectManager
QRect CalcButtonRect(const QRect& contentRect) const;
QRect CalcSummaryRect(const QRect& contentRect, bool hasTags) const;
void DrawPlatformIcons(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const;
void DrawButton(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const;
void DrawButton(QPainter* painter, const QRect& buttonRect, const QModelIndex& modelIndex) const;
void DrawFeatureTags(QPainter* painter, const QRect& contentRect, const QStringList& featureTags, const QFont& standardFont, const QRect& summaryRect) const;
void DrawText(const QString& text, QPainter* painter, const QRect& rect, const QFont& standardFont) const;
void DrawDownloadStatusIcon(QPainter* painter, const QRect& contentRect, const QRect& buttonRect, const QModelIndex& modelIndex) const;
QAbstractItemModel* m_model = nullptr;
@ -85,5 +89,14 @@ namespace O3DE::ProjectManager
void AddPlatformIcon(GemInfo::Platform platform, const QString& iconPath);
inline constexpr static int s_platformIconSize = 12;
QHash<GemInfo::Platform, QPixmap> m_platformIcons;
// Status icons
void SetStatusIcon(QPixmap& m_iconPixmap, const QString& iconPath);
inline constexpr static int s_statusIconSize = 16;
inline constexpr static int s_statusButtonSpacing = 5;
QPixmap m_unknownStatusPixmap;
QPixmap m_notDownloadedPixmap;
QMovie* m_downloadingMovie = nullptr;
};
} // namespace O3DE::ProjectManager

@ -103,11 +103,11 @@ namespace O3DE::ProjectManager
QSpacerItem* horizontalSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
columnHeaderLayout->addSpacerItem(horizontalSpacer);
QLabel* gemSelectedLabel = new QLabel(tr("Selected"));
QLabel* gemSelectedLabel = new QLabel(tr("Status"));
gemSelectedLabel->setObjectName("GemCatalogHeaderLabel");
columnHeaderLayout->addWidget(gemSelectedLabel);
columnHeaderLayout->addSpacing(65);
columnHeaderLayout->addSpacing(72);
vLayout->addLayout(columnHeaderLayout);
}

@ -9,6 +9,8 @@
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemItemDelegate.h>
#include <QMovie>
namespace O3DE::ProjectManager
{
GemListView::GemListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent)
@ -19,6 +21,17 @@ namespace O3DE::ProjectManager
setModel(model);
setSelectionModel(selectionModel);
setItemDelegate(new GemItemDelegate(model, this));
GemItemDelegate* itemDelegate = new GemItemDelegate(model, this);
connect(itemDelegate, &GemItemDelegate::MovieStartedPlaying, [=](const QMovie* playingMovie)
{
// Force redraw when movie is playing so animation is smooth
connect(playingMovie, &QMovie::frameChanged, this, [=]
{
this->viewport()->repaint();
});
});
setItemDelegate(itemDelegate);
}
} // namespace O3DE::ProjectManager

@ -48,6 +48,7 @@ namespace O3DE::ProjectManager
item->setData(gemInfo.m_features, RoleFeatures);
item->setData(gemInfo.m_path, RolePath);
item->setData(gemInfo.m_requirement, RoleRequirement);
item->setData(gemInfo.m_downloadStatus, RoleDownloadStatus);
appendRow(item);
@ -132,6 +133,11 @@ namespace O3DE::ProjectManager
return static_cast<GemInfo::Types>(modelIndex.data(RoleTypes).toInt());
}
GemInfo::DownloadStatus GemModel::GetDownloadStatus(const QModelIndex& modelIndex)
{
return static_cast<GemInfo::DownloadStatus>(modelIndex.data(RoleDownloadStatus).toInt());
}
QString GemModel::GetSummary(const QModelIndex& modelIndex)
{
return modelIndex.data(RoleSummary).toString();
@ -373,6 +379,11 @@ namespace O3DE::ProjectManager
return previouslyAdded && !added;
}
void GemModel::SetDownloadStatus(QAbstractItemModel& model, const QModelIndex& modelIndex, GemInfo::DownloadStatus status)
{
model.setData(modelIndex, status, RoleDownloadStatus);
}
bool GemModel::HasRequirement(const QModelIndex& modelIndex)
{
return !modelIndex.data(RoleRequirement).toString().isEmpty();

@ -40,6 +40,7 @@ namespace O3DE::ProjectManager
static GemInfo::GemOrigin GetGemOrigin(const QModelIndex& modelIndex);
static GemInfo::Platforms GetPlatforms(const QModelIndex& modelIndex);
static GemInfo::Types GetTypes(const QModelIndex& modelIndex);
static GemInfo::DownloadStatus GetDownloadStatus(const QModelIndex& modelIndex);
static QString GetSummary(const QModelIndex& modelIndex);
static QString GetDirectoryLink(const QModelIndex& modelIndex);
static QString GetDocLink(const QModelIndex& modelIndex);
@ -64,6 +65,7 @@ namespace O3DE::ProjectManager
static bool NeedsToBeRemoved(const QModelIndex& modelIndex, bool includeDependencies = false);
static bool HasRequirement(const QModelIndex& modelIndex);
static void UpdateDependencies(QAbstractItemModel& model, const QModelIndex& modelIndex);
static void SetDownloadStatus(QAbstractItemModel& model, const QModelIndex& modelIndex, GemInfo::DownloadStatus status);
bool DoGemsToBeAddedHaveRequirements() const;
bool HasDependentGemsToRemove() const;
@ -101,7 +103,8 @@ namespace O3DE::ProjectManager
RoleFeatures,
RoleTypes,
RolePath,
RoleRequirement
RoleRequirement,
RoleDownloadStatus
};
QHash<QString, QModelIndex> m_nameToIndexMap;

@ -668,7 +668,21 @@ namespace O3DE::ProjectManager
if (gemInfo.m_creator.contains("Open 3D Engine"))
{
gemInfo.m_gemOrigin = GemInfo::GemOrigin::Open3DEEngine;
gemInfo.m_gemOrigin = GemInfo::GemOrigin::Open3DEngine;
}
else if (gemInfo.m_creator.contains("Amazon Web Services"))
{
gemInfo.m_gemOrigin = GemInfo::GemOrigin::Local;
}
else if (data.contains("origin"))
{
gemInfo.m_gemOrigin = GemInfo::GemOrigin::Remote;
}
// As long Base Open3DEngine gems are installed before first startup non-remote gems will be downloaded
if (gemInfo.m_gemOrigin != GemInfo::GemOrigin::Remote)
{
gemInfo.m_downloadStatus = GemInfo::DownloadStatus::Downloaded;
}
if (data.contains("user_tags"))

@ -33,7 +33,7 @@ namespace AWSClientAuth
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
if (serialize)
{
serialize->Class<AWSClientAuthSystemComponent, AZ::Component>()->Version(1);
serialize->Class<AWSClientAuthSystemComponent, AZ::Component>()->Version(2);
if (AZ::EditContext* ec = serialize->GetEditContext())
{
@ -105,12 +105,22 @@ namespace AWSClientAuth
behaviorContext->EBus<AWSCognitoUserManagementRequestBus>("AWSCognitoUserManagementRequestBus")
->Attribute(AZ::Script::Attributes::Category, SerializeComponentName)
->Event("Initialize", &AWSCognitoUserManagementRequestBus::Events::Initialize)
->Event("EmailSignUpAsync", &AWSCognitoUserManagementRequestBus::Events::EmailSignUpAsync)
->Event("PhoneSignUpAsync", &AWSCognitoUserManagementRequestBus::Events::PhoneSignUpAsync)
->Event("ConfirmSignUpAsync", &AWSCognitoUserManagementRequestBus::Events::ConfirmSignUpAsync)
->Event("ForgotPasswordAsync", &AWSCognitoUserManagementRequestBus::Events::ForgotPasswordAsync)
->Event("ConfirmForgotPasswordAsync", &AWSCognitoUserManagementRequestBus::Events::ConfirmForgotPasswordAsync)
->Event("EnableMFAAsync", &AWSCognitoUserManagementRequestBus::Events::EnableMFAAsync);
->Event(
"EmailSignUpAsync", &AWSCognitoUserManagementRequestBus::Events::EmailSignUpAsync,
{ { { "Username", "The client's username" }, { "Password", "The client's password" }, { "Email", "The email address used to sign up" } } })
->Event(
"PhoneSignUpAsync", &AWSCognitoUserManagementRequestBus::Events::PhoneSignUpAsync,
{ { { "Username", "The client's username" }, { "Password", "The client's password" }, { "Phone number", "The phone number used to sign up" } } })
->Event(
"ConfirmSignUpAsync", &AWSCognitoUserManagementRequestBus::Events::ConfirmSignUpAsync,
{ { { "Username", "The client's username" }, { "Confirmation code", "The client's confirmation code" } } })
->Event(
"ForgotPasswordAsync", &AWSCognitoUserManagementRequestBus::Events::ForgotPasswordAsync,
{ { { "Username", "The client's username" } } })
->Event(
"ConfirmForgotPasswordAsync", &AWSCognitoUserManagementRequestBus::Events::ConfirmForgotPasswordAsync,
{ { { "Username", "The client's username" }, { "Confirmation code", "The client's confirmation code" }, { "New password", "The new password for the client" } } })
->Event("EnableMFAAsync", &AWSCognitoUserManagementRequestBus::Events::EnableMFAAsync, { { { "Access token", "The MFA access token" } } });
behaviorContext->EBus<AuthenticationProviderNotificationBus>("AuthenticationProviderNotificationBus")

@ -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

@ -10,36 +10,12 @@
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/string/string.h>
#include <AzFramework/Matchmaking/MatchmakingRequests.h>
#include <AWSGameLiftPlayer.h>
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<AZStd::string, int> 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<AZStd::string, AZStd::string> 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<AWSGameLiftPlayerInformation> m_players;
AZStd::vector<AWSGameLiftPlayer> m_players;
};
} // namespace AWSGameLift

@ -10,6 +10,7 @@
#include <AzCore/std/bind/bind.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AzFramework/Session/ISessionHandlingRequests.h>
#include <AzFramework/Matchmaking/MatchmakingNotifications.h>
#include <AWSGameLiftClientLocalTicketTracker.h>
#include <AWSGameLiftSessionConstants.h>
@ -109,6 +110,7 @@ namespace AWSGameLift
else if (ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::REQUIRES_ACCEPTANCE)
{
// broadcast acceptance requires to player
AzFramework::MatchAcceptanceNotificationBus::Broadcast(&AzFramework::MatchAcceptanceNotifications::OnMatchAcceptance);
}
else
{

@ -18,6 +18,7 @@
#include <AWSGameLiftClientManager.h>
#include <AWSGameLiftSessionConstants.h>
#include <Activity/AWSGameLiftAcceptMatchActivity.h>
#include <Activity/AWSGameLiftCreateSessionActivity.h>
#include <Activity/AWSGameLiftCreateSessionOnQueueActivity.h>
#include <Activity/AWSGameLiftJoinSessionActivity.h>
@ -125,12 +126,53 @@ namespace AWSGameLift
void AWSGameLiftClientManager::AcceptMatch(const AzFramework::AcceptMatchRequest& acceptMatchRequest)
{
AZ_UNUSED(acceptMatchRequest);
if (AcceptMatchActivity::ValidateAcceptMatchRequest(acceptMatchRequest))
{
const AWSGameLiftAcceptMatchRequest& gameliftStartMatchmakingRequest =
static_cast<const AWSGameLiftAcceptMatchRequest&>(acceptMatchRequest);
AcceptMatchHelper(gameliftStartMatchmakingRequest);
}
}
void AWSGameLiftClientManager::AcceptMatchAsync(const AzFramework::AcceptMatchRequest& acceptMatchRequest)
{
AZ_UNUSED(acceptMatchRequest);
if (!AcceptMatchActivity::ValidateAcceptMatchRequest(acceptMatchRequest))
{
AzFramework::MatchmakingAsyncRequestNotificationBus::Broadcast(
&AzFramework::MatchmakingAsyncRequestNotifications::OnAcceptMatchAsyncComplete);
return;
}
const AWSGameLiftAcceptMatchRequest& gameliftStartMatchmakingRequest = static_cast<const AWSGameLiftAcceptMatchRequest&>(acceptMatchRequest);
AZ::JobContext* jobContext = nullptr;
AWSCore::AWSCoreRequestBus::BroadcastResult(jobContext, &AWSCore::AWSCoreRequests::GetDefaultJobContext);
AZ::Job* acceptMatchJob = AZ::CreateJobFunction(
[this, gameliftStartMatchmakingRequest]()
{
AcceptMatchHelper(gameliftStartMatchmakingRequest);
AzFramework::MatchmakingAsyncRequestNotificationBus::Broadcast(
&AzFramework::MatchmakingAsyncRequestNotifications::OnAcceptMatchAsyncComplete);
},
true, jobContext);
acceptMatchJob->Start();
}
void AWSGameLiftClientManager::AcceptMatchHelper(const AWSGameLiftAcceptMatchRequest& acceptMatchRequest)
{
auto gameliftClient = AZ::Interface<IAWSGameLiftInternalRequests>::Get()->GetGameLiftClient();
AZStd::string response;
if (!gameliftClient)
{
AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftClientMissingErrorMessage);
}
else
{
AcceptMatchActivity::AcceptMatch(*gameliftClient, acceptMatchRequest);
}
}
AZStd::string AWSGameLiftClientManager::CreateSession(const AzFramework::CreateSessionRequest& createSessionRequest)

@ -15,6 +15,7 @@
namespace AWSGameLift
{
struct AWSGameLiftAcceptMatchRequest;
struct AWSGameLiftCreateSessionRequest;
struct AWSGameLiftCreateSessionOnQueueRequest;
struct AWSGameLiftJoinSessionRequest;
@ -158,6 +159,7 @@ namespace AWSGameLift
void LeaveSession() override;
private:
void AcceptMatchHelper(const AWSGameLiftAcceptMatchRequest& createSessionRequest);
AZStd::string CreateSessionHelper(const AWSGameLiftCreateSessionRequest& createSessionRequest);
AZStd::string CreateSessionOnQueueHelper(const AWSGameLiftCreateSessionOnQueueRequest& createSessionOnQueueRequest);
bool JoinSessionHelper(const AWSGameLiftJoinSessionRequest& joinSessionRequest);

@ -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))

@ -0,0 +1,76 @@
/*
* 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.AcceptMatch
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Interface/Interface.h>
#include <Activity/AWSGameLiftAcceptMatchActivity.h>
#include <AWSGameLiftSessionConstants.h>
#include <aws/core/utils/Outcome.h>
#include <aws/gamelift/model/AcceptMatchRequest.h>
namespace AWSGameLift
{
namespace AcceptMatchActivity
{
Aws::GameLift::Model::AcceptMatchRequest BuildAWSGameLiftAcceptMatchRequest(
const AWSGameLiftAcceptMatchRequest& acceptMatchRequest)
{
Aws::GameLift::Model::AcceptMatchRequest request;
request.SetAcceptanceType(acceptMatchRequest.m_acceptMatch ?
Aws::GameLift::Model::AcceptanceType::ACCEPT : Aws::GameLift::Model::AcceptanceType::REJECT);
Aws::Vector<Aws::String> playerIds;
for (const AZStd::string& playerId : acceptMatchRequest.m_playerIds)
{
playerIds.emplace_back(playerId.c_str());
}
request.SetPlayerIds(playerIds);
if (!acceptMatchRequest.m_ticketId.empty())
{
request.SetTicketId(acceptMatchRequest.m_ticketId.c_str());
}
AZ_TracePrintf(AWSGameLiftAcceptMatchActivityName, "Built AcceptMatchRequest with TicketId=%s", request.GetTicketId().c_str());
return request;
}
void AcceptMatch(const Aws::GameLift::GameLiftClient& gameliftClient,
const AWSGameLiftAcceptMatchRequest& AcceptMatchRequest)
{
AZ_TracePrintf(AWSGameLiftAcceptMatchActivityName, "Requesting AcceptMatch against Amazon GameLift service ...");
Aws::GameLift::Model::AcceptMatchRequest request = BuildAWSGameLiftAcceptMatchRequest(AcceptMatchRequest);
auto AcceptMatchOutcome = gameliftClient.AcceptMatch(request);
if (AcceptMatchOutcome.IsSuccess())
{
AZ_TracePrintf(AWSGameLiftAcceptMatchActivityName, "AcceptMatch request against Amazon GameLift service is complete");
}
else
{
AZ_Error(AWSGameLiftAcceptMatchActivityName, false, AWSGameLiftErrorMessageTemplate,
AcceptMatchOutcome.GetError().GetExceptionName().c_str(), AcceptMatchOutcome.GetError().GetMessage().c_str());
}
}
bool ValidateAcceptMatchRequest(const AzFramework::AcceptMatchRequest& AcceptMatchRequest)
{
auto gameliftAcceptMatchRequest = azrtti_cast<const AWSGameLiftAcceptMatchRequest*>(&AcceptMatchRequest);
bool isValid = gameliftAcceptMatchRequest &&
(gameliftAcceptMatchRequest->m_playerIds.size() > 0) &&
(!gameliftAcceptMatchRequest->m_ticketId.empty());
AZ_Error(AWSGameLiftAcceptMatchActivityName, isValid, AWSGameLiftAcceptMatchRequestInvalidErrorMessage);
return isValid;
}
} // namespace AcceptMatchActivity
} // namespace AWSGameLift

@ -0,0 +1,31 @@
/*
* 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 <Request/AWSGameLiftAcceptMatchRequest.h>
#include <aws/gamelift/GameLiftClient.h>
namespace AWSGameLift
{
namespace AcceptMatchActivity
{
static constexpr const char AWSGameLiftAcceptMatchActivityName[] = "AWSGameLiftAcceptMatchActivity";
static constexpr const char AWSGameLiftAcceptMatchRequestInvalidErrorMessage[] = "Invalid GameLift AcceptMatch request.";
// Build AWS GameLift AcceptMatchRequest by using AWSGameLiftAcceptMatchRequest
Aws::GameLift::Model::AcceptMatchRequest BuildAWSGameLiftAcceptMatchRequest(const AWSGameLiftAcceptMatchRequest& AcceptMatchRequest);
// Create AcceptMatchRequest and make a AcceptMatch call through GameLift client
void AcceptMatch(const Aws::GameLift::GameLiftClient& gameliftClient, const AWSGameLiftAcceptMatchRequest& AcceptMatchRequest);
// Validate AcceptMatchRequest and check required request parameters
bool ValidateAcceptMatchRequest(const AzFramework::AcceptMatchRequest& AcceptMatchRequest);
} // namespace AcceptMatchActivity
} // namespace AWSGameLift

@ -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();

@ -5,12 +5,17 @@
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Interface/Interface.h>
#include <Activity/AWSGameLiftActivityUtils.h>
#include <Activity/AWSGameLiftStartMatchmakingActivity.h>
#include <AWSGameLiftPlayer.h>
#include <AWSGameLiftSessionConstants.h>
#include <aws/core/utils/Outcome.h>
#include <aws/gamelift/model/StartMatchmakingRequest.h>
namespace AWSGameLift
{
namespace StartMatchmakingActivity
@ -25,7 +30,7 @@ namespace AWSGameLift
}
Aws::Vector<Aws::GameLift::Model::Player> 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())
@ -105,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);

@ -10,9 +10,7 @@
#include <Request/AWSGameLiftStartMatchmakingRequest.h>
#include <aws/core/utils/Outcome.h>
#include <aws/gamelift/GameLiftClient.h>
#include <aws/gamelift/model/StartMatchmakingRequest.h>
namespace AWSGameLift
{
@ -25,7 +23,6 @@ namespace AWSGameLift
Aws::GameLift::Model::StartMatchmakingRequest BuildAWSGameLiftStartMatchmakingRequest(const AWSGameLiftStartMatchmakingRequest& startMatchmakingRequest);
// Create StartMatchmakingRequest and make a StartMatchmaking call through GameLift client
// Will also start polling the matchmaking ticket when get success outcome from GameLift client
AZStd::string StartMatchmaking(const Aws::GameLift::GameLiftClient& gameliftClient, const AWSGameLiftStartMatchmakingRequest& startMatchmakingRequest);
// Validate StartMatchmakingRequest and check required request parameters

@ -11,6 +11,9 @@
#include <Activity/AWSGameLiftStopMatchmakingActivity.h>
#include <AWSGameLiftSessionConstants.h>
#include <aws/core/utils/Outcome.h>
#include <aws/gamelift/model/StopMatchmakingRequest.h>
namespace AWSGameLift
{
namespace StopMatchmakingActivity

@ -10,9 +10,7 @@
#include <Request/AWSGameLiftStopMatchmakingRequest.h>
#include <aws/core/utils/Outcome.h>
#include <aws/gamelift/GameLiftClient.h>
#include <aws/gamelift/model/StopMatchmakingRequest.h>
namespace AWSGameLift
{
@ -25,7 +23,6 @@ namespace AWSGameLift
Aws::GameLift::Model::StopMatchmakingRequest BuildAWSGameLiftStopMatchmakingRequest(const AWSGameLiftStopMatchmakingRequest& stopMatchmakingRequest);
// Create StopMatchmakingRequest and make a StopMatchmaking call through GameLift client
// Will also stop polling the matchmaking ticket when get success outcome from GameLift client
void StopMatchmaking(const Aws::GameLift::GameLiftClient& gameliftClient, const AWSGameLiftStopMatchmakingRequest& stopMatchmakingRequest);
// Validate StopMatchmakingRequest and check required request parameters

@ -14,53 +14,10 @@
namespace AWSGameLift
{
void AWSGameLiftPlayerInformation::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<AWSGameLiftPlayerInformation>()
->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>("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<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<AWSGameLiftPlayerInformation>("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<AZ::SerializeContext*>(context))
{

@ -351,3 +351,41 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallAndTicketComple
WaitForProcessFinish();
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_RequiresAcceptanceAndTicketCompleteAtLast_ProcessContinuesAndStop)
{
Aws::GameLift::Model::MatchmakingTicket ticket1;
ticket1.SetStatus(Aws::GameLift::Model::MatchmakingConfigurationStatus::REQUIRES_ACCEPTANCE);
Aws::GameLift::Model::DescribeMatchmakingResult result1;
result1.AddTicketList(ticket1);
Aws::GameLift::Model::DescribeMatchmakingOutcome outcome1(result1);
Aws::GameLift::Model::GameSessionConnectionInfo connectionInfo;
connectionInfo.SetIpAddress("DummyIpAddress");
connectionInfo.SetPort(123);
connectionInfo.AddMatchedPlayerSessions(
Aws::GameLift::Model::MatchedPlayerSession().WithPlayerId("player1").WithPlayerSessionId("playersession1"));
Aws::GameLift::Model::MatchmakingTicket ticket2;
ticket2.SetStatus(Aws::GameLift::Model::MatchmakingConfigurationStatus::COMPLETED);
ticket2.SetGameSessionConnectionInfo(connectionInfo);
Aws::GameLift::Model::DescribeMatchmakingResult result2;
result2.AddTicketList(ticket2);
Aws::GameLift::Model::DescribeMatchmakingOutcome outcome2(result2);
EXPECT_CALL(*m_gameliftClientMockPtr, DescribeMatchmaking(::testing::_))
.WillOnce(::testing::Return(outcome1))
.WillOnce(::testing::Return(outcome2));
MatchAcceptanceNotificationsHandlerMock handlerMock1;
EXPECT_CALL(handlerMock1, OnMatchAcceptance()).Times(1);
SessionHandlingClientRequestsMock handlerMock2;
EXPECT_CALL(handlerMock2, RequestPlayerJoinSession(::testing::_)).Times(1).WillOnce(::testing::Return(true));
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish();
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}

@ -19,6 +19,7 @@
#include <AWSGameLiftClientManager.h>
#include <AWSGameLiftClientMocks.h>
#include <Request/AWSGameLiftAcceptMatchRequest.h>
#include <Request/AWSGameLiftCreateSessionOnQueueRequest.h>
#include <Request/AWSGameLiftCreateSessionRequest.h>
#include <Request/AWSGameLiftJoinSessionRequest.h>
@ -207,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";
@ -231,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;
@ -812,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);
@ -854,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);
@ -1005,3 +1007,106 @@ TEST_F(AWSGameLiftClientManagerTest, StopMatchmakingAsync_CallWithValidRequest_G
m_gameliftClientManager->StopMatchmakingAsync(request);
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
}
TEST_F(AWSGameLiftClientManagerTest, AcceptMatch_CallWithoutClientSetup_GetError)
{
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientManager->ConfigureGameLiftClient("");
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_playerIds = { DummyPlayerId };
request.m_ticketId = DummyMatchmakingTicketId;
m_gameliftClientManager->AcceptMatch(request);
AZ_TEST_STOP_TRACE_SUPPRESSION(2); // capture 2 error message
}
TEST_F(AWSGameLiftClientManagerTest, AcceptMatch_CallWithInvalidRequest_GetError)
{
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientManager->AcceptMatch(AzFramework::AcceptMatchRequest());
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
}
TEST_F(AWSGameLiftClientManagerTest, AcceptMatch_CallWithValidRequest_Success)
{
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_playerIds = { DummyPlayerId };
request.m_ticketId = DummyMatchmakingTicketId;
Aws::GameLift::Model::AcceptMatchResult result;
Aws::GameLift::Model::AcceptMatchResult outcome(result);
EXPECT_CALL(*m_gameliftClientMockPtr, AcceptMatch(::testing::_)).Times(1).WillOnce(::testing::Return(outcome));
m_gameliftClientManager->AcceptMatch(request);
}
TEST_F(AWSGameLiftClientManagerTest, AcceptMatch_CallWithValidRequest_GetError)
{
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_playerIds = { DummyPlayerId };
request.m_ticketId = DummyMatchmakingTicketId;
Aws::Client::AWSError<Aws::GameLift::GameLiftErrors> error;
Aws::GameLift::Model::AcceptMatchOutcome outcome(error);
EXPECT_CALL(*m_gameliftClientMockPtr, AcceptMatch(::testing::_)).Times(1).WillOnce(::testing::Return(outcome));
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientManager->AcceptMatch(request);
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
}
TEST_F(AWSGameLiftClientManagerTest, AcceptMatchAsync_CallWithInvalidRequest_GetNotificationWithError)
{
AWSGameLiftAcceptMatchRequest request;
MatchmakingAsyncRequestNotificationsHandlerMock matchmakingHandlerMock;
EXPECT_CALL(matchmakingHandlerMock, OnAcceptMatchAsyncComplete()).Times(1);
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientManager->AcceptMatchAsync(request);
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
}
TEST_F(AWSGameLiftClientManagerTest, AcceptMatchAsync_CallWithValidRequest_GetNotification)
{
AWSCoreRequestsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get()));
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_playerIds = { DummyPlayerId };
request.m_ticketId = DummyMatchmakingTicketId;
Aws::GameLift::Model::AcceptMatchResult result;
Aws::GameLift::Model::AcceptMatchOutcome outcome(result);
EXPECT_CALL(*m_gameliftClientMockPtr, AcceptMatch(::testing::_)).Times(1).WillOnce(::testing::Return(outcome));
MatchmakingAsyncRequestNotificationsHandlerMock matchmakingHandlerMock;
EXPECT_CALL(matchmakingHandlerMock, OnAcceptMatchAsyncComplete()).Times(1);
m_gameliftClientManager->AcceptMatchAsync(request);
}
TEST_F(AWSGameLiftClientManagerTest, AcceptMatchAsync_CallWithValidRequest_GetNotificationWithError)
{
AWSCoreRequestsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get()));
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_playerIds = { DummyPlayerId };
request.m_ticketId = DummyMatchmakingTicketId;
Aws::Client::AWSError<Aws::GameLift::GameLiftErrors> error;
Aws::GameLift::Model::AcceptMatchOutcome outcome(error);
EXPECT_CALL(*m_gameliftClientMockPtr, AcceptMatch(::testing::_)).Times(1).WillOnce(::testing::Return(outcome));
MatchmakingAsyncRequestNotificationsHandlerMock matchmakingHandlerMock;
EXPECT_CALL(matchmakingHandlerMock, OnAcceptMatchAsyncComplete()).Times(1);
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientManager->AcceptMatchAsync(request);
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
}

@ -9,6 +9,7 @@
#pragma once
#include <AzCore/Interface/Interface.h>
#include <AzFramework/Matchmaking/MatchmakingNotifications.h>
#include <AzFramework/Session/ISessionRequests.h>
#include <AzFramework/Session/ISessionHandlingRequests.h>
#include <AzFramework/Matchmaking/MatchmakingNotifications.h>
@ -18,6 +19,8 @@
#include <aws/core/utils/Outcome.h>
#include <aws/gamelift/GameLiftClient.h>
#include <aws/gamelift/GameLiftErrors.h>
#include <aws/gamelift/model/AcceptMatchRequest.h>
#include <aws/gamelift/model/AcceptMatchResult.h>
#include <aws/gamelift/model/CreateGameSessionRequest.h>
#include <aws/gamelift/model/CreateGameSessionResult.h>
#include <aws/gamelift/model/CreatePlayerSessionRequest.h>
@ -46,6 +49,7 @@ public:
{
}
MOCK_CONST_METHOD1(AcceptMatch, Model::AcceptMatchOutcome(const Model::AcceptMatchRequest&));
MOCK_CONST_METHOD1(CreateGameSession, Model::CreateGameSessionOutcome(const Model::CreateGameSessionRequest&));
MOCK_CONST_METHOD1(CreatePlayerSession, Model::CreatePlayerSessionOutcome(const Model::CreatePlayerSessionRequest&));
MOCK_CONST_METHOD1(DescribeMatchmaking, Model::DescribeMatchmakingOutcome(const Model::DescribeMatchmakingRequest&));
@ -74,6 +78,23 @@ public:
MOCK_METHOD0(OnStopMatchmakingAsyncComplete, void());
};
class MatchAcceptanceNotificationsHandlerMock
: public AzFramework::MatchAcceptanceNotificationBus::Handler
{
public:
MatchAcceptanceNotificationsHandlerMock()
{
AzFramework::MatchAcceptanceNotificationBus::Handler::BusConnect();
}
~MatchAcceptanceNotificationsHandlerMock()
{
AzFramework::MatchAcceptanceNotificationBus::Handler::BusDisconnect();
}
MOCK_METHOD0(OnMatchAcceptance, void());
};
class SessionAsyncRequestNotificationsHandlerMock
: public AzFramework::SessionAsyncRequestNotificationBus::Handler
{

@ -0,0 +1,77 @@
/*
* 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 <aws/gamelift/model/AcceptMatchRequest.h>
#include <AWSGameLiftClientFixture.h>
#include <Activity/AWSGameLiftAcceptMatchActivity.h>
#include <aws/gamelift/model/AcceptMatchRequest.h>
using namespace AWSGameLift;
using AWSGameLiftAcceptMatchActivityTest = AWSGameLiftClientFixture;
TEST_F(AWSGameLiftAcceptMatchActivityTest, BuildAWSGameLiftAcceptMatchRequest_Call_GetExpectedResult)
{
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_ticketId = "dummyTicketId";
request.m_playerIds = { "dummyPlayerId" };
auto awsRequest = AcceptMatchActivity::BuildAWSGameLiftAcceptMatchRequest(request);
EXPECT_EQ(awsRequest.GetAcceptanceType(), Aws::GameLift::Model::AcceptanceType::ACCEPT);
EXPECT_TRUE(strcmp(awsRequest.GetTicketId().c_str(), request.m_ticketId.c_str()) == 0);
EXPECT_EQ(awsRequest.GetPlayerIds().size(), request.m_playerIds.size());
EXPECT_TRUE(strcmp(awsRequest.GetPlayerIds().begin()->c_str(), request.m_playerIds.begin()->c_str()) == 0);
}
TEST_F(AWSGameLiftAcceptMatchActivityTest, ValidateAcceptMatchRequest_CallWithBaseType_GetFalseResult)
{
AZ_TEST_START_TRACE_SUPPRESSION;
auto result = AcceptMatchActivity::ValidateAcceptMatchRequest(AzFramework::AcceptMatchRequest());
EXPECT_FALSE(result);
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
}
TEST_F(AWSGameLiftAcceptMatchActivityTest, ValidateAcceptMatchRequest_CallWithoutTicketId_GetFalseResult)
{
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_playerIds = { "dummyPlayerId" };
AZ_TEST_START_TRACE_SUPPRESSION;
auto result = AcceptMatchActivity::ValidateAcceptMatchRequest(request);
EXPECT_FALSE(result);
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
}
TEST_F(AWSGameLiftAcceptMatchActivityTest, ValidateAcceptMatchRequest_CallWithoutPlayerIds_GetFalseResult)
{
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_playerIds = { "dummyPlayerId" };
AZ_TEST_START_TRACE_SUPPRESSION;
auto result = AcceptMatchActivity::ValidateAcceptMatchRequest(request);
EXPECT_FALSE(result);
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
}
TEST_F(AWSGameLiftAcceptMatchActivityTest, ValidateAcceptMatchRequest_CallWithValidAttributes_GetTrueResult)
{
AWSGameLiftAcceptMatchRequest request;
request.m_acceptMatch = true;
request.m_ticketId = "dummyTicketId";
request.m_playerIds = { "dummyPlayerId" };
auto result = AcceptMatchActivity::ValidateAcceptMatchRequest(request);
EXPECT_TRUE(result);
}

@ -8,6 +8,9 @@
#include <Activity/AWSGameLiftStartMatchmakingActivity.h>
#include <AWSGameLiftClientFixture.h>
#include <AWSGameLiftPlayer.h>
#include <aws/gamelift/model/StartMatchmakingRequest.h>
using namespace AWSGameLift;
@ -19,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";
@ -56,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";
@ -87,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;
@ -105,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";
@ -123,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";
@ -140,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";

@ -9,6 +9,8 @@
#include <Activity/AWSGameLiftStopMatchmakingActivity.h>
#include <AWSGameLiftClientFixture.h>
#include <aws/gamelift/model/StopMatchmakingRequest.h>
using namespace AWSGameLift;
using AWSGameLiftStopMatchmakingActivityTest = AWSGameLiftClientFixture;

@ -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
@ -18,6 +20,8 @@ set(FILES
Include/Request/IAWSGameLiftRequests.h
Source/Activity/AWSGameLiftActivityUtils.cpp
Source/Activity/AWSGameLiftActivityUtils.h
Source/Activity/AWSGameLiftAcceptMatchActivity.cpp
Source/Activity/AWSGameLiftAcceptMatchActivity.h
Source/Activity/AWSGameLiftCreateSessionActivity.cpp
Source/Activity/AWSGameLiftCreateSessionActivity.h
Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.cpp

@ -7,6 +7,7 @@
#
set(FILES
Tests/Activity/AWSGameLiftAcceptMatchActivityTest.cpp
Tests/Activity/AWSGameLiftCreateSessionActivityTest.cpp
Tests/Activity/AWSGameLiftCreateSessionOnQueueActivityTest.cpp
Tests/Activity/AWSGameLiftJoinSessionActivityTest.cpp

@ -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 <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/string/string.h>
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<AZStd::string, int> 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<AZStd::string, AZStd::string> 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

@ -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 <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AWSGameLiftPlayer.h>
namespace AWSGameLift
{
void AWSGameLiftPlayer::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<AWSGameLiftPlayer>()
->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>("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<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<AWSGameLiftPlayer>("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

@ -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

@ -9,9 +9,11 @@
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/string/string.h>
#include <AzFramework/Session/ISessionRequests.h>
#include <AWSGameLiftPlayer.h>
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<AWSGameLiftPlayer>& 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

@ -17,6 +17,9 @@
#include <AzCore/IO/SystemFile.h>
#include <AzCore/Jobs/JobFunction.h>
#include <AzCore/Jobs/JobManagerBus.h>
#include <AzCore/JSON/error/en.h>
#include <AzCore/JSON/stringbuffer.h>
#include <AzCore/JSON/writer.h>
#include <AzCore/std/bind/bind.h>
#include <AzFramework/Session/SessionNotifications.h>
@ -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<AWSGameLiftPlayer> AWSGameLiftServerManager::GetActiveServerMatchBackfillPlayers()
{
AZStd::vector<AWSGameLiftPlayer> 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<Aws::GameLift::Server::Model::PlayerSession> 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<Aws::GameLift::Server::Model::PlayerSession> 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<rapidjson::StringBuffer> 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<AWSGameLiftPlayer>& 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<AWSGameLiftPlayer> 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<AWSGameLiftPlayer>& 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;

@ -11,11 +11,15 @@
#include <aws/gamelift/server/GameLiftServerAPI.h>
#include <aws/gamelift/server/model/GameSession.h>
#include <AzCore/JSON/rapidjson.h>
#include <AzCore/JSON/document.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Session/ISessionHandlingRequests.h>
#include <AzFramework/Session/SessionConfig.h>
#include <AWSGameLiftPlayer.h>
#include <Request/IAWSGameLiftServerRequests.h>
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<AWSGameLiftPlayer>& 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<AWSGameLiftPlayer> 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<AWSGameLiftPlayer>& 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<Aws::GameLift::Server::Model::PlayerSession> 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<PlayerConnectionId, PlayerSessionId> 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

@ -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

@ -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

@ -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

@ -8,6 +8,7 @@
#pragma once
#include <AWSGameLiftPlayer.h>
#include <AWSGameLiftServerSystemComponent.h>
#include <AWSGameLiftServerManager.h>
#include <GameLiftServerSDKWrapper.h>
@ -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<bool()> m_healthCheckFunc;
AZStd::function<void()> m_onProcessTerminateFunc;
AZStd::function<void(Aws::GameLift::Server::Model::GameSession)> m_onStartGameSessionFunc;
AZStd::function<void(Aws::GameLift::Server::Model::UpdateGameSession)> 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<AWSGameLiftPlayer> GetTestServerMatchBackfillPlayers()
{
return GetActiveServerMatchBackfillPlayers();
}
NiceMock<GameLiftServerSDKWrapperMock>* m_gameLiftServerSDKWrapperMockPtr;
Aws::GameLift::Server::Model::GameSession m_testGameSession;
};
class AWSGameLiftServerSystemComponentMock

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save