Merge branch 'development' of https://github.com/o3de/o3de into sc-editor-asset-redux

Signed-off-by: chcurran <82187351+carlitosan@users.noreply.github.com>
This commit is contained in:
chcurran
2021-11-11 16:04:56 -08:00
1944 changed files with 220327 additions and 6146 deletions
+1 -30
View File
@@ -249,38 +249,9 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
RUNTIME_DEPENDENCIES
Gem::LmbrCentral
)
ly_add_googletest(
NAME Legacy::EditorLib.Tests
)
ly_add_target(
NAME EditorLib.Camera.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE Legacy
FILES_CMAKE
Lib/Tests/Camera/editor_lib_camera_test_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
.
BUILD_DEPENDENCIES
PRIVATE
AZ::AzCore
AZ::AzTest
AZ::AzToolsFramework
AZ::AzTestShared
Legacy::EditorLib
Gem::Camera.Editor
Gem::AtomToolsFramework.Static
RUNTIME_DEPENDENCIES
Legacy::EditorLib
)
ly_add_source_properties(
SOURCES Lib/Tests/Camera/test_EditorCamera.cpp
PROPERTY COMPILE_DEFINITIONS
VALUES CAMERA_EDITOR_MODULE="$<TARGET_FILE_BASE_NAME:Camera.Editor>"
)
ly_add_googletest(
NAME Legacy::EditorLib.Camera.Tests
)
endif()
+2 -1
View File
@@ -3827,7 +3827,8 @@ void CCryEditApp::OnOpenQuickAccessBar()
}
QRect geo = m_pQuickAccessBar->geometry();
geo.moveCenter(MainWindow::instance()->geometry().center());
auto mainWindow = MainWindow::instance();
geo.moveCenter(mainWindow->mapToGlobal(mainWindow->geometry().center()));
m_pQuickAccessBar->setGeometry(geo);
m_pQuickAccessBar->setVisible(true);
m_pQuickAccessBar->setFocus();
+1 -1
View File
@@ -17,7 +17,7 @@ void SetEditorEnvironment(SSystemGlobalEnvironment* pEnv)
void AttachEditorAZEnvironment(AZ::EnvironmentInstance azEnv)
{
AZ::Environment::Attach(azEnv, true);
AZ::Environment::Attach(azEnv);
}
void DetachEditorAZEnvironment()
@@ -13,9 +13,32 @@
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Render/IntersectorInterface.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h>
#include <EditorViewportSettings.h>
AZ_CVAR(
bool,
ed_cameraPinDefaultOrbit,
true,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets whether the default orbit point moves with the camera or not");
AZ_CVAR(
bool,
ed_cameraDefaultOrbitAxesOrtho,
true,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets whether to draw the default orbit point as orthographic or not");
AZ_CVAR(
float,
ed_cameraDefaultOrbitFadeDuration,
0.5f,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets how long the default orbit point should take to appear and disappear");
namespace SandboxEditor
{
static AzFramework::TranslateCameraInputChannelIds BuildTranslateCameraInputChannelIds()
@@ -174,7 +197,7 @@ namespace SandboxEditor
return SandboxEditor::CameraScrollSpeed();
};
const auto pivotFn = []
const auto pivotFn = []() -> AZStd::optional<AZ::Vector3>
{
// use the manipulator transform as the pivot point
AZStd::optional<AZ::Transform> entityPivot;
@@ -187,8 +210,7 @@ namespace SandboxEditor
return entityPivot->GetTranslation();
}
// otherwise just use the identity
return AZ::Vector3::CreateZero();
return AZStd::nullopt;
};
m_firstPersonFocusCamera =
@@ -199,9 +221,26 @@ namespace SandboxEditor
m_orbitCamera = AZStd::make_shared<AzFramework::OrbitCameraInput>(SandboxEditor::CameraOrbitChannelId());
m_orbitCamera->SetPivotFn(
[pivotFn]([[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
[this, pivotFn](const AZ::Vector3& position, const AZ::Vector3& direction)
{
return pivotFn();
// return the pivot
if (auto pivot = pivotFn())
{
return pivot.value();
}
// start ticking and drawing (for the default pivot)
AZ::TickBus::Handler::BusConnect();
AzFramework::ViewportDebugDisplayEventBus::Handler::BusConnect(AzToolsFramework::GetEntityContextId());
m_defaultOrbiting = true;
// calculate the default orbit point
if (!ed_cameraPinDefaultOrbit || m_orbitCamera->Beginning())
{
m_defaultOrbitPoint = position + direction * SandboxEditor::CameraDefaultOrbitDistance();
}
return m_defaultOrbitPoint;
});
m_orbitRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraOrbitLookChannelId());
@@ -306,4 +345,67 @@ namespace SandboxEditor
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame);
}
}
void EditorModularViewportCameraComposer::OnTick(const float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
{
const float delta = [duration = &ed_cameraDefaultOrbitFadeDuration, deltaTime] {
if (*duration == 0.0f) {
return 1.0f;
}
return deltaTime / *duration;
}();
if (m_defaultOrbiting)
{
m_defaultOrbitOpacity = AZStd::min(m_defaultOrbitOpacity + delta, 1.0f);
}
else
{
m_defaultOrbitOpacity = AZStd::max(m_defaultOrbitOpacity - delta, 0.0f);
if (m_defaultOrbitOpacity == 0.0f)
{
AZ::TickBus::Handler::BusDisconnect();
AzFramework::ViewportDebugDisplayEventBus::Handler::BusDisconnect();
}
}
m_defaultOrbiting = false;
}
static void DrawTransformAxis(
AzFramework::DebugDisplayRequests& display,
const AzFramework::CameraState& cameraState,
const AZ::Vector3& pivot,
const float axisLength,
const float alpha)
{
const int prevState = display.GetState();
display.DepthWriteOff();
display.DepthTestOff();
display.CullOff();
const float orthoScale =
ed_cameraDefaultOrbitAxesOrtho ? AzToolsFramework::CalculateScreenToWorldMultiplier(pivot, cameraState) : 1.0f;
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::Red.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisX() * axisLength * orthoScale);
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::LawnGreen.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisY() * axisLength * orthoScale);
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::Blue.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisZ() * axisLength * orthoScale);
display.DepthWriteOn();
display.DepthTestOn();
display.CullOn();
display.SetState(prevState);
}
void EditorModularViewportCameraComposer::DisplayViewport(
[[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay)
{
DrawTransformAxis(
debugDisplay, AzToolsFramework::GetCameraState(viewportInfo.m_viewportId), m_defaultOrbitPoint, 1.0f, m_defaultOrbitOpacity);
}
} // namespace SandboxEditor
@@ -9,6 +9,8 @@
#pragma once
#include <AtomToolsFramework/Viewport/ModularViewportCameraController.h>
#include <AzCore/Component/TickBus.h>
#include <AzFramework/Entity/EntityDebugDisplayBus.h>
#include <AzFramework/Viewport/CameraInput.h>
#include <AzToolsFramework/API/EditorCameraBus.h>
#include <EditorModularViewportCameraComposerBus.h>
@@ -20,6 +22,8 @@ namespace SandboxEditor
class EditorModularViewportCameraComposer
: private EditorModularViewportCameraComposerNotificationBus::Handler
, private Camera::EditorCameraNotificationBus::Handler
, private AzFramework::ViewportDebugDisplayEventBus::Handler
, private AZ::TickBus::Handler
{
public:
SANDBOX_API explicit EditorModularViewportCameraComposer(AzFramework::ViewportId viewportId);
@@ -29,6 +33,12 @@ namespace SandboxEditor
SANDBOX_API AZStd::shared_ptr<AtomToolsFramework::ModularViewportCameraController> CreateModularViewportCameraController();
private:
// AzFramework::ViewportDebugDisplayEventBus overrides ...
void DisplayViewport(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) override;
// AZ::TickBus overrides ...
void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
//! Setup all internal camera inputs.
void SetupCameras();
@@ -52,5 +62,9 @@ namespace SandboxEditor
AZStd::shared_ptr<AzFramework::FocusCameraInput> m_orbitFocusCamera;
AzFramework::ViewportId m_viewportId;
float m_defaultOrbitOpacity = 0.0f; //!< The default orbit axes opacity (to fade in and out).
AZ::Vector3 m_defaultOrbitPoint = AZ::Vector3::CreateZero(); //!< The orbit point to use when no entity is selected.
bool m_defaultOrbiting = false; //!< Is the camera default orbiting (orbiting when there's no selected entity).
};
} // namespace SandboxEditor
@@ -61,7 +61,7 @@ static AZStd::vector<AZStd::string> GetEditorInputNames()
void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serialize)
{
serialize.Class<CameraMovementSettings>()
->Version(3)
->Version(4)
->Field("TranslateSpeed", &CameraMovementSettings::m_translateSpeed)
->Field("RotateSpeed", &CameraMovementSettings::m_rotateSpeed)
->Field("BoostMultiplier", &CameraMovementSettings::m_boostMultiplier)
@@ -76,9 +76,8 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Field("OrbitYawRotationInverted", &CameraMovementSettings::m_orbitYawRotationInverted)
->Field("PanInvertedX", &CameraMovementSettings::m_panInvertedX)
->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY)
->Field("DefaultPositionX", &CameraMovementSettings::m_defaultCameraPositionX)
->Field("DefaultPositionY", &CameraMovementSettings::m_defaultCameraPositionY)
->Field("DefaultPositionZ", &CameraMovementSettings::m_defaultCameraPositionZ);
->Field("DefaultPosition", &CameraMovementSettings::m_defaultPosition)
->Field("DefaultOrbitDistance", &CameraMovementSettings::m_defaultOrbitDistance);
serialize.Class<CameraInputSettings>()
->Version(2)
@@ -159,14 +158,12 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_captureCursorLook, "Camera Capture Look Cursor",
"Should the cursor be captured (hidden) while performing free look")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionX, "Default X Camera Position",
"Default X Camera Position when a level is opened")
AZ::Edit::UIHandlers::Vector3, &CameraMovementSettings::m_defaultPosition, "Default Camera Position",
"Default Camera Position when a level is first opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionY, "Default Y Camera Position",
"Default Y Camera Position when a level is opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionZ, "Default Z Camera Position",
"Default Z Camera Position when a level is opened");
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultOrbitDistance, "Default Orbit Distance",
"The default distance to orbit about when there is no entity selected")
->Attribute(AZ::Edit::Attributes::Min, minValue);
editContext->Class<CameraInputSettings>("Camera Input Settings", "")
->DataElement(
@@ -283,12 +280,8 @@ void CEditorPreferencesPage_ViewportCamera::OnApply()
SandboxEditor::SetCameraOrbitYawRotationInverted(m_cameraMovementSettings.m_orbitYawRotationInverted);
SandboxEditor::SetCameraPanInvertedX(m_cameraMovementSettings.m_panInvertedX);
SandboxEditor::SetCameraPanInvertedY(m_cameraMovementSettings.m_panInvertedY);
SandboxEditor::SetDefaultCameraEditorPosition(
AZ::Vector3(
m_cameraMovementSettings.m_defaultCameraPositionX,
m_cameraMovementSettings.m_defaultCameraPositionY,
m_cameraMovementSettings.m_defaultCameraPositionZ
));
SandboxEditor::SetCameraDefaultEditorPosition(m_cameraMovementSettings.m_defaultPosition);
SandboxEditor::SetCameraDefaultOrbitDistance(m_cameraMovementSettings.m_defaultOrbitDistance);
SandboxEditor::SetCameraTranslateForwardChannelId(m_cameraInputSettings.m_translateForwardChannelId);
SandboxEditor::SetCameraTranslateBackwardChannelId(m_cameraInputSettings.m_translateBackwardChannelId);
@@ -325,11 +318,8 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings()
m_cameraMovementSettings.m_orbitYawRotationInverted = SandboxEditor::CameraOrbitYawRotationInverted();
m_cameraMovementSettings.m_panInvertedX = SandboxEditor::CameraPanInvertedX();
m_cameraMovementSettings.m_panInvertedY = SandboxEditor::CameraPanInvertedY();
AZ::Vector3 defaultCameraPosition = SandboxEditor::DefaultEditorCameraPosition();
m_cameraMovementSettings.m_defaultCameraPositionX = defaultCameraPosition.GetX();
m_cameraMovementSettings.m_defaultCameraPositionY = defaultCameraPosition.GetY();
m_cameraMovementSettings.m_defaultCameraPositionZ = defaultCameraPosition.GetZ();
m_cameraMovementSettings.m_defaultPosition = SandboxEditor::CameraDefaultEditorPosition();
m_cameraMovementSettings.m_defaultOrbitDistance = SandboxEditor::CameraDefaultOrbitDistance();
m_cameraInputSettings.m_translateForwardChannelId = SandboxEditor::CameraTranslateForwardChannelId().GetName();
m_cameraInputSettings.m_translateBackwardChannelId = SandboxEditor::CameraTranslateBackwardChannelId().GetName();
@@ -9,9 +9,12 @@
#pragma once
#include "Include/IPreferencesPage.h"
#include <AzCore/Math/Vector3.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <QIcon>
inline AZ::Crc32 EditorPropertyVisibility(const bool enabled)
@@ -43,6 +46,7 @@ private:
{
AZ_TYPE_INFO(CameraMovementSettings, "{60B8C07E-5F48-4171-A50B-F45558B5CCA1}")
AZ::Vector3 m_defaultPosition;
float m_translateSpeed;
float m_rotateSpeed;
float m_scrollSpeed;
@@ -50,16 +54,14 @@ private:
float m_panSpeed;
float m_boostMultiplier;
float m_rotateSmoothness;
bool m_rotateSmoothing;
float m_translateSmoothness;
bool m_translateSmoothing;
float m_defaultOrbitDistance;
bool m_captureCursorLook;
bool m_orbitYawRotationInverted;
bool m_panInvertedX;
bool m_panInvertedY;
float m_defaultCameraPositionX;
float m_defaultCameraPositionY;
float m_defaultCameraPositionZ;
bool m_rotateSmoothing;
bool m_translateSmoothing;
AZ::Crc32 RotateSmoothingVisibility() const
{
+17 -6
View File
@@ -38,6 +38,7 @@ namespace SandboxEditor
constexpr AZStd::string_view CameraTranslateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/TranslateSmoothing";
constexpr AZStd::string_view CameraRotateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/RotateSmoothing";
constexpr AZStd::string_view CameraCaptureCursorLookSetting = "/Amazon/Preferences/Editor/Camera/CaptureCursorLook";
constexpr AZStd::string_view CameraDefaultOrbitDistanceSetting = "/Amazon/Preferences/Editor/Camera/DefaultOrbitDistance";
constexpr AZStd::string_view CameraTranslateForwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateForwardId";
constexpr AZStd::string_view CameraTranslateBackwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateBackwardId";
constexpr AZStd::string_view CameraTranslateLeftIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateLeftId";
@@ -114,15 +115,15 @@ namespace SandboxEditor
return AZStd::make_unique<EditorViewportSettingsCallbacksImpl>();
}
AZ::Vector3 DefaultEditorCameraPosition()
AZ::Vector3 CameraDefaultEditorPosition()
{
float xPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionX, 0.0));
float yPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionY, -10.0));
float zPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionZ, 4.0));
return AZ::Vector3(xPosition, yPosition, zPosition);
return AZ::Vector3(
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionX, 0.0)),
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionY, -10.0)),
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionZ, 4.0)));
}
void SetDefaultCameraEditorPosition(const AZ::Vector3 defaultCameraPosition)
void SetCameraDefaultEditorPosition(const AZ::Vector3& defaultCameraPosition)
{
SetRegistry(CameraDefaultStartingPositionX, defaultCameraPosition.GetX());
SetRegistry(CameraDefaultStartingPositionY, defaultCameraPosition.GetY());
@@ -359,6 +360,16 @@ namespace SandboxEditor
SetRegistry(CameraCaptureCursorLookSetting, capture);
}
float CameraDefaultOrbitDistance()
{
return aznumeric_cast<float>(GetRegistry(CameraDefaultOrbitDistanceSetting, 20.0));
}
void SetCameraDefaultOrbitDistance(const float distance)
{
SetRegistry(CameraDefaultOrbitDistanceSetting, distance);
}
AzFramework::InputChannelId CameraTranslateForwardChannelId()
{
return AzFramework::InputChannelId(
+6 -3
View File
@@ -33,9 +33,6 @@ namespace SandboxEditor
//! event will fire when a value in the settings registry (editorpreferences.setreg) is modified.
SANDBOX_API AZStd::unique_ptr<EditorViewportSettingsCallbacks> CreateEditorViewportSettingsCallbacks();
SANDBOX_API AZ::Vector3 DefaultEditorCameraPosition();
SANDBOX_API void SetDefaultCameraEditorPosition(AZ::Vector3 defaultCameraPosition);
SANDBOX_API AZ::u64 MaxItemsShownInAssetBrowserSearch();
SANDBOX_API void SetMaxItemsShownInAssetBrowserSearch(AZ::u64 numberOfItemsShown);
@@ -105,6 +102,12 @@ namespace SandboxEditor
SANDBOX_API bool CameraCaptureCursorForLook();
SANDBOX_API void SetCameraCaptureCursorForLook(bool capture);
SANDBOX_API AZ::Vector3 CameraDefaultEditorPosition();
SANDBOX_API void SetCameraDefaultEditorPosition(const AZ::Vector3& position);
SANDBOX_API float CameraDefaultOrbitDistance();
SANDBOX_API void SetCameraDefaultOrbitDistance(float distance);
SANDBOX_API AzFramework::InputChannelId CameraTranslateForwardChannelId();
SANDBOX_API void SetCameraTranslateForwardChannelId(AZStd::string_view cameraTranslateForwardId);
+37 -11
View File
@@ -43,10 +43,11 @@
// AzToolsFramework
#include <AzToolsFramework/API/ComponentEntityObjectBus.h>
#include <AzToolsFramework/API/EditorCameraBus.h>
#include <AzToolsFramework/API/ViewportEditorModeTrackerInterface.h>
#include <AzToolsFramework/Manipulators/ManipulatorManager.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
#include <AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h>
#include <AzToolsFramework/API/EditorCameraBus.h>
// AtomToolsFramework
#include <AtomToolsFramework/Viewport/RenderViewportWidget.h>
@@ -619,9 +620,9 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
break;
case eNotify_OnEndNewScene:
PopDisableRendering();
{
PopDisableRendering();
Matrix34 viewTM;
viewTM.SetIdentity();
@@ -637,9 +638,9 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
break;
case eNotify_OnEndTerrainCreate:
PopDisableRendering();
{
PopDisableRendering();
Matrix34 viewTM;
viewTM.SetIdentity();
@@ -1032,6 +1033,7 @@ void EditorViewportWidget::ConnectViewportInteractionRequestBus()
AzToolsFramework::ViewportInteraction::MainEditorViewportInteractionRequestBus::Handler::BusConnect(GetViewportId());
AzToolsFramework::ViewportInteraction::EditorEntityViewportInteractionRequestBus::Handler::BusConnect(GetViewportId());
m_viewportUi.ConnectViewportUiBus(GetViewportId());
AzFramework::ViewportBorderRequestBus::Handler::BusConnect(GetViewportId());
AzFramework::InputSystemCursorConstraintRequestBus::Handler::BusConnect();
}
@@ -1040,6 +1042,7 @@ void EditorViewportWidget::DisconnectViewportInteractionRequestBus()
{
AzFramework::InputSystemCursorConstraintRequestBus::Handler::BusDisconnect();
AzFramework::ViewportBorderRequestBus::Handler::BusDisconnect();
m_viewportUi.DisconnectViewportUiBus();
AzToolsFramework::ViewportInteraction::EditorEntityViewportInteractionRequestBus::Handler::BusDisconnect();
AzToolsFramework::ViewportInteraction::MainEditorViewportInteractionRequestBus::Handler::BusDisconnect();
@@ -1124,7 +1127,9 @@ void EditorViewportWidget::OnTitleMenu(QMenu* menu)
action = menu->addAction(tr("Create camera entity from current view"));
connect(action, &QAction::triggered, this, &EditorViewportWidget::OnMenuCreateCameraEntityFromCurrentView);
if (!gameEngine || !gameEngine->IsLevelLoaded())
const auto prefabEditorEntityOwnershipInterface = AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
if (!gameEngine || !gameEngine->IsLevelLoaded() ||
(prefabEditorEntityOwnershipInterface && !prefabEditorEntityOwnershipInterface->IsRootPrefabAssigned()))
{
action->setEnabled(false);
action->setToolTip(tr(AZ::ViewportHelpers::TextCantCreateCameraNoLevel));
@@ -2016,10 +2021,6 @@ void EditorViewportWidget::SetDefaultCamera()
GetViewManager()->SetCameraObjectId(GUID_NULL);
SetName(m_defaultViewName);
// Set the default Editor Camera position.
m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(m_defaultViewTM);
// Synchronize the configured editor viewport FOV to the default camera
if (m_viewPane)
{
@@ -2036,6 +2037,10 @@ void EditorViewportWidget::SetDefaultCamera()
atomViewportRequests->PushView(contextName, m_defaultView);
}
// Set the default Editor Camera position.
m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(m_defaultViewTM);
PostCameraSet();
}
@@ -2522,7 +2527,7 @@ bool EditorViewportSettings::StickySelectEnabled() const
AZ::Vector3 EditorViewportSettings::DefaultEditorCameraPosition() const
{
return SandboxEditor::DefaultEditorCameraPosition();
return SandboxEditor::CameraDefaultEditorPosition();
}
AZ_CVAR_EXTERNED(bool, ed_previewGameInFullscreen_once);
@@ -2636,4 +2641,25 @@ void EditorViewportWidget::StopFullscreenPreview()
// Show the main window
MainWindow::instance()->show();
}
AZStd::optional<AzFramework::ViewportBorderPadding> EditorViewportWidget::GetViewportBorderPadding() const
{
if (auto viewportEditorModeTracker = AZ::Interface<AzToolsFramework::ViewportEditorModeTrackerInterface>::Get())
{
auto viewportEditorModes = viewportEditorModeTracker->GetViewportEditorModes({ AzToolsFramework::GetEntityContextId() });
if (viewportEditorModes->IsModeActive(AzToolsFramework::ViewportEditorMode::Focus) ||
viewportEditorModes->IsModeActive(AzToolsFramework::ViewportEditorMode::Component))
{
AzFramework::ViewportBorderPadding viewportBorderPadding = {};
viewportBorderPadding.m_top = AzToolsFramework::ViewportUi::ViewportUiTopBorderSize;
viewportBorderPadding.m_left = AzToolsFramework::ViewportUi::ViewportUiLeftRightBottomBorderSize;
viewportBorderPadding.m_right = AzToolsFramework::ViewportUi::ViewportUiLeftRightBottomBorderSize;
viewportBorderPadding.m_bottom = AzToolsFramework::ViewportUi::ViewportUiLeftRightBottomBorderSize;
return viewportBorderPadding;
}
}
return AZStd::nullopt;
}
#include <moc_EditorViewportWidget.cpp>
+5
View File
@@ -38,6 +38,7 @@
#include <AzFramework/Windowing/WindowBus.h>
#include <AzFramework/Visibility/EntityVisibilityQuery.h>
#include <AzFramework/Viewport/ViewportBus.h>
// forward declarations.
class CBaseObject;
@@ -86,6 +87,7 @@ AZ_PUSH_DISABLE_DLL_EXPORT_BASECLASS_WARNING
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
class SANDBOX_API EditorViewportWidget final
: public QtViewport
, public AzFramework::ViewportBorderRequestBus::Handler
, private IEditorNotifyListener
, private IUndoManagerListener
, private Camera::EditorCameraRequestBus::Handler
@@ -120,6 +122,9 @@ public:
void SetFOV(float fov) override;
float GetFOV() const override;
// AzFramework::ViewportBorderRequestBus overrides ...
AZStd::optional<AzFramework::ViewportBorderPadding> GetViewportBorderPadding() const override;
private:
////////////////////////////////////////////////////////////////////////
// Private types ...
+9 -20
View File
@@ -49,9 +49,6 @@
#include "Include/IObjectManager.h"
#include "ActionManager.h"
// Including this too early will result in a linker error
#include <CryCommon/CryLibrary.h>
// Implementation of System Callback structure.
struct SSystemUserCallback
: public ISystemUserCallback
@@ -242,8 +239,7 @@ private:
AZ_PUSH_DISABLE_WARNING(4273, "-Wunknown-warning-option")
CGameEngine::CGameEngine()
: m_gameDll(nullptr)
, m_bIgnoreUpdates(false)
: m_bIgnoreUpdates(false)
, m_ePendingGameMode(ePGM_NotPending)
, m_modalWindowDismisser(nullptr)
AZ_POP_DISABLE_WARNING
@@ -253,7 +249,7 @@ AZ_POP_DISABLE_WARNING
m_bInGameMode = false;
m_bSimulationMode = false;
m_bSyncPlayerPosition = true;
m_hSystemHandle = nullptr;
m_hSystemHandle.reset(nullptr);
m_bJustCreated = false;
m_levelName = "Untitled";
m_levelExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
@@ -268,18 +264,10 @@ AZ_POP_DISABLE_WARNING
GetIEditor()->UnregisterNotifyListener(this);
m_pISystem->GetIMovieSystem()->SetCallback(nullptr);
if (m_gameDll)
{
CryFreeLibrary(m_gameDll);
}
delete m_pISystem;
m_pISystem = nullptr;
if (m_hSystemHandle)
{
CryFreeLibrary(m_hSystemHandle);
}
m_hSystemHandle.reset(nullptr);
delete m_pSystemUserCallback;
}
@@ -347,18 +335,19 @@ AZ::Outcome<void, AZStd::string> CGameEngine::Init(
HWND hwndForInputSystem)
{
m_pSystemUserCallback = new SSystemUserCallback(logo);
m_hSystemHandle = CryLoadLibraryDefName("CrySystem");
if (!m_hSystemHandle)
constexpr const char* crySystemLibraryName = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX "CrySystem" AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION;
m_hSystemHandle = AZ::DynamicModuleHandle::Create(crySystemLibraryName);
if (!m_hSystemHandle->Load(true))
{
auto errorMessage = AZStd::string::format("%s Loading Failed", CryLibraryDefName("CrySystem"));
auto errorMessage = AZStd::string::format("%s Loading Failed", crySystemLibraryName);
Error(errorMessage.c_str());
return AZ::Failure(errorMessage);
}
PFNCREATESYSTEMINTERFACE pfnCreateSystemInterface =
(PFNCREATESYSTEMINTERFACE)CryGetProcAddress(m_hSystemHandle, "CreateSystemInterface");
m_hSystemHandle->GetFunction<PFNCREATESYSTEMINTERFACE>("CreateSystemInterface");
SSystemInitParams sip;
+3 -7
View File
@@ -28,6 +28,8 @@ struct IInitializeUIInfo;
#include <AzCore/Interface/Interface.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Module/DynamicModuleHandle.h>
class ThreadedOnErrorHandler : public QObject
{
Q_OBJECT
@@ -124,11 +126,6 @@ public:
return s_pakModifyMutex;
}
inline HMODULE GetGameModule() const
{
return m_gameDll;
}
private:
void SetGameMode(bool inGame);
void SwitchToInGame();
@@ -150,8 +147,7 @@ private:
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
Matrix34 m_playerViewTM;
struct SSystemUserCallback* m_pSystemUserCallback;
HMODULE m_hSystemHandle;
HMODULE m_gameDll;
AZStd::unique_ptr<AZ::DynamicModuleHandle> m_hSystemHandle;
enum EPendingGameMode
{
ePGM_NotPending,
@@ -1,11 +0,0 @@
#
# 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
#
#
set(FILES
test_EditorCamera.cpp
)
@@ -17,43 +17,33 @@
namespace UnitTest
{
class EditorCameraTestEnvironment : public AZ::Test::GemTestEnvironment
{
// AZ::Test::GemTestEnvironment overrides ...
void AddGemsAndComponents() override;
};
void EditorCameraTestEnvironment::AddGemsAndComponents()
{
AddDynamicModulePaths({ CAMERA_EDITOR_MODULE });
AddComponentDescriptors({ AzToolsFramework::Components::TransformComponent::CreateDescriptor() });
}
class EditorCameraFixture : public ::testing::Test
{
public:
AZ::ComponentApplication* m_application = nullptr;
AtomToolsFramework::ModularCameraViewportContext* m_cameraViewportContextView = nullptr;
AZStd::unique_ptr<SandboxEditor::EditorModularViewportCameraComposer> m_editorModularViewportCameraComposer;
AZStd::unique_ptr<AZ::DynamicModuleHandle> m_editorLibHandle;
AzFramework::ViewportControllerListPtr m_controllerList;
AZStd::unique_ptr<AZ::Entity> m_entity;
AZ::Entity* m_entity = nullptr;
AZ::ComponentDescriptor* m_transformComponent = nullptr;
static const AzFramework::ViewportId TestViewportId;
void SetUp() override
{
m_editorLibHandle = AZ::DynamicModuleHandle::Create("EditorLib");
[[maybe_unused]] const bool loaded = m_editorLibHandle->Load(true);
AZ_Assert(loaded, "EditorLib could not be loaded");
m_application = aznew AZ::ComponentApplication;
AZ::ComponentApplication::Descriptor appDesc;
m_entity = m_application->Create(appDesc);
m_transformComponent = AzToolsFramework::Components::TransformComponent::CreateDescriptor();
m_application->RegisterComponentDescriptor(m_transformComponent);
m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
m_controllerList->RegisterViewportContext(TestViewportId);
m_entity = AZStd::make_unique<AZ::Entity>();
m_entity->Init();
m_entity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
m_entity->Activate();
m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
m_controllerList->RegisterViewportContext(TestViewportId);
m_editorModularViewportCameraComposer = AZStd::make_unique<SandboxEditor::EditorModularViewportCameraComposer>(TestViewportId);
auto controller = m_editorModularViewportCameraComposer->CreateModularViewportCameraController();
@@ -72,8 +62,17 @@ namespace UnitTest
{
m_editorModularViewportCameraComposer.reset();
m_cameraViewportContextView = nullptr;
m_entity.reset();
m_editorLibHandle = {};
if (m_application)
{
m_application->UnregisterComponentDescriptor(m_transformComponent);
delete m_transformComponent;
m_transformComponent = nullptr;
m_application->Destroy();
delete m_application;
m_application = nullptr;
}
}
};
@@ -211,15 +210,3 @@ namespace UnitTest
EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity()));
}
} // namespace UnitTest
// required to support running integration tests with the Camera Gem
AZTEST_EXPORT int AZ_UNIT_TEST_HOOK_NAME(int argc, char** argv)
{
::testing::InitGoogleMock(&argc, argv);
AZ::Test::printUnusedParametersWarning(argc, argv);
AZ::Test::addTestEnvironments({ new UnitTest::EditorCameraTestEnvironment() });
int result = RUN_ALL_TESTS();
return result;
}
IMPLEMENT_TEST_EXECUTABLE_MAIN();
+8 -3
View File
@@ -9,12 +9,13 @@
#include "EditorDefs.h"
#include <AzTest/AzTest.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/UnitTest/UnitTest.h>
#include <EditorEnvironment.h>
#include <QApplication>
class EditorLibTestEnvironment
: public AZ::Test::ITestEnvironment
: public ::UnitTest::TraceBusHook
{
public:
~EditorLibTestEnvironment() override = default;
@@ -22,16 +23,20 @@ public:
protected:
void SetupEnvironment() override
{
::UnitTest::TraceBusHook::SetupEnvironment();
AZ::Environment::Create(nullptr);
AttachEditorAZEnvironment(AZ::Environment::GetInstance());
AZ::AllocatorInstance<AZ::SystemAllocator>::Create();
AttachEditorAZEnvironment(AZ::Environment::GetInstance());
}
void TeardownEnvironment() override
{
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
DetachEditorAZEnvironment();
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
AZ::Environment::Destroy();
::UnitTest::TraceBusHook::TeardownEnvironment();
}
};
+1 -1
View File
@@ -44,7 +44,7 @@ namespace LyViewPane
static const char* const SubstanceEditor = "Substance Editor";
static const char* const VegetationEditor = "Vegetation Editor";
static const char* const LandscapeCanvas = "Landscape Canvas";
static const char* const AnimationEditor = "EMotion FX Animation Editor (PREVIEW)";
static const char* const AnimationEditor = "EMotion FX Animation Editor";
static const char* const PhysXConfigurationEditor = "PhysX Configuration (PREVIEW)";
static const char* const SliceRelationships = "Slice Relationship View (PREVIEW)";
-1
View File
@@ -27,7 +27,6 @@
// Editor
#include "MainStatusBarItems.h"
#include "CryLibrary.h"
#include "ProcessInfo.h"
+1 -1
View File
@@ -1061,7 +1061,7 @@ void MainWindow::InitActions()
if (emfxEnabled.value)
{
QAction* action = am->AddAction(ID_OPEN_EMOTIONFX_EDITOR, tr("Animation Editor"))
.SetToolTip(tr("Open Animation Editor (PREVIEW)"))
.SetToolTip(tr("Open Animation Editor"))
.SetIcon(QIcon(":/EMotionFX/EMFX_icon_32x32.png"))
.SetApplyHoverEffect();
QObject::connect(action, &QAction::triggered, this, []() {
@@ -10,6 +10,8 @@
#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
#include <AzFramework/XcbEventHandler.h>
#include <AzFramework/XcbConnectionManager.h>
#include <qpa/qplatformnativeinterface.h>
#endif
namespace Editor
@@ -23,16 +25,34 @@ namespace Editor
return nullptr;
}
xcb_connection_t* EditorQtApplicationXcb::GetXcbConnectionFromQt()
{
QPlatformNativeInterface* native = platformNativeInterface();
AZ_Warning("EditorQtApplicationXcb", native, "Unable to retrieve the native platform interface");
if (!native)
{
return nullptr;
}
return reinterpret_cast<xcb_connection_t*>(native->nativeResourceForIntegration(QByteArray("connection")));
}
void EditorQtApplicationXcb::OnStartPlayInEditor()
{
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
interface->SetEnableXInput(GetXcbConnectionFromQt(), true);
}
void EditorQtApplicationXcb::OnStopPlayInEditor()
{
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
interface->SetEnableXInput(GetXcbConnectionFromQt(), false);
}
bool EditorQtApplicationXcb::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long*)
{
if (GetIEditor()->IsInGameMode())
{
#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
// We need to handle RAW Input events in a separate loop. This is a workaround to enable XInput2 RAW Inputs using Editor mode.
// TODO To have this call here might be not be perfect.
AzFramework::XcbEventHandlerBus::Broadcast(&AzFramework::XcbEventHandler::PollSpecialEvents);
// Now handle the rest of the events.
AzFramework::XcbEventHandlerBus::Broadcast(
&AzFramework::XcbEventHandler::HandleXcbEvent, static_cast<xcb_generic_event_t*>(message));
#endif
@@ -6,19 +6,35 @@
*
*/
#if !defined(Q_MOC_RUN)
#include <Editor/Core/QtEditorApplication.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#endif
using xcb_connection_t = struct xcb_connection_t;
namespace Editor
{
class EditorQtApplicationXcb : public EditorQtApplication
class EditorQtApplicationXcb
: public EditorQtApplication
, public AzToolsFramework::EditorEntityContextNotificationBus::Handler
{
Q_OBJECT
public:
EditorQtApplicationXcb(int& argc, char** argv)
: EditorQtApplication(argc, argv)
{
// Connect bus to listen for OnStart/StopPlayInEditor events
AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusConnect();
}
xcb_connection_t* GetXcbConnectionFromQt();
///////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorEntityContextNotificationBus overrides
void OnStartPlayInEditor() override;
void OnStopPlayInEditor() override;
// QAbstractNativeEventFilter:
bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override;
};
+1
View File
@@ -22,6 +22,7 @@ set(FILES
Lib/Tests/test_DisplaySettingsPythonBindings.cpp
Lib/Tests/test_ViewportManipulatorController.cpp
Lib/Tests/test_ModularViewportCameraController.cpp
Lib/Tests/Camera/test_EditorCamera.cpp
DisplaySettingsPythonFuncs.cpp
DisplaySettingsPythonFuncs.h
)
@@ -70,6 +70,9 @@ namespace AZ::Data
const char* GetFilename() const override { return m_filePath.c_str(); }
AZStd::chrono::milliseconds GetStreamingDeadline() const { return m_curDeadline; }
AZ::IO::IStreamerTypes::Priority GetStreamingPriority() const { return m_curPriority; }
// AssetDataStream specific APIs
//! Whether or not all data has been loaded.
@@ -214,7 +214,6 @@ namespace AZ
// Update old Project path before attempting to merge in new Settings Registry values in order to prevent recursive calls
m_oldProjectPath = newProjectPath;
// Merge the project.json file into settings registry under ProjectSettingsRootKey path.
// Update all the runtime file paths based on the new "project_path" value.
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(m_registry);
}
@@ -457,8 +457,6 @@ void JobManagerWorkStealing::ProcessJobsInternal(ThreadInfo* info, Job* suspende
else
{
//attempt to steal a job from another thread's queue
AZ_PROFILE_SCOPE(AzCore, "JobManagerWorkStealing::ProcessJobsInternal:WorkStealing");
unsigned int numStealAttempts = 0;
const unsigned int maxStealAttempts = (unsigned int)m_workerThreads.size() * 3; //try every thread a few times before giving up
while (!job)
@@ -3408,7 +3408,14 @@ LUA_API const Node* lua_getDummyNode()
const BehaviorParameter* arg = method->GetArgument(iArg);
BehaviorClass* argClass = nullptr;
LuaLoadFromStack fromStack = FromLuaStack(context, arg, argClass);
AZ_Assert(fromStack, "Argument %s for Method %s doesn't have support to be converted to Lua!", arg->m_name, method->m_name.c_str());
AZ_Assert(fromStack,
"The argument type: %s for method: %s is not serialized and/or reflected for scripting.\n"
"Make sure %s is added to the SerializeContext and reflected to the BehaviorContext\n"
"For example, verify these two exist and are being called in a Reflect function:\n"
"serializeContext->Class<%s>();\n"
"behaviorContext->Class<%s>();\n"
"%s will not be available for scripting unless these requirements are met."
, arg->m_name, method->m_name.c_str(), arg->m_name, arg->m_name, arg->m_name, method->m_name.c_str());
m_fromLua.push_back(AZStd::make_pair(fromStack, argClass));
}
@@ -29,6 +29,8 @@
namespace AZ::Internal
{
static constexpr const char* ProductCacheDirectoryName = "Cache";
AZ::SettingsRegistryInterface::FixedValueString GetEngineMonikerForProject(
SettingsRegistryInterface& settingsRegistry, const AZ::IO::FixedMaxPath& projectJsonPath)
{
@@ -228,19 +230,20 @@ namespace AZ::Internal
namespace AZ::SettingsRegistryMergeUtils
{
constexpr AZStd::string_view InternalScanUpEngineRootKey{ "/O3DE/Settings/Internal/engine_root_scan_up_path" };
constexpr AZStd::string_view InternalScanUpProjectRootKey{ "/O3DE/Settings/Internal/project_root_scan_up_path" };
AZ::IO::FixedMaxPath FindEngineRoot(SettingsRegistryInterface& settingsRegistry)
{
static constexpr AZStd::string_view InternalScanUpEngineRootKey{ "/O3DE/Runtime/Internal/engine_root_scan_up_path" };
using FixedValueString = SettingsRegistryInterface::FixedValueString;
using Type = SettingsRegistryInterface::Type;
AZ::IO::FixedMaxPath engineRoot;
// This is the 'external' engine root key, as in passed from command-line or .setreg files.
auto engineRootKey = SettingsRegistryInterface::FixedValueString::format("%s/engine_path", BootstrapSettingsRootKey);
constexpr auto engineRootKey = FixedValueString(BootstrapSettingsRootKey) + "/engine_path";
// Step 1 Run the scan upwards logic once to find the location of the engine.json if it exist
// Once this step is run the {InternalScanUpEngineRootKey} is set in the Settings Registry
// to have this scan logic only run once InternalScanUpEngineRootKey the supplied registry
if (settingsRegistry.GetType(InternalScanUpEngineRootKey) == SettingsRegistryInterface::Type::NoType)
if (settingsRegistry.GetType(InternalScanUpEngineRootKey) == Type::NoType)
{
// We can scan up from exe directory to find engine.json, use that for engine root if it exists.
engineRoot = Internal::ScanUpRootLocator("engine.json");
@@ -283,14 +286,18 @@ namespace AZ::SettingsRegistryMergeUtils
AZ::IO::FixedMaxPath FindProjectRoot(SettingsRegistryInterface& settingsRegistry)
{
AZ::IO::FixedMaxPath projectRoot;
const auto projectRootKey = SettingsRegistryInterface::FixedValueString::format("%s/project_path", BootstrapSettingsRootKey);
static constexpr AZStd::string_view InternalScanUpProjectRootKey{ "/O3DE/Runtime/Internal/project_root_scan_up_path" };
using FixedValueString = SettingsRegistryInterface::FixedValueString;
using Type = SettingsRegistryInterface::Type;
// Step 1 Run the scan upwards logic once to find the location of the project.json if it exist
AZ::IO::FixedMaxPath projectRoot;
constexpr auto projectRootKey = FixedValueString(BootstrapSettingsRootKey) + "/project_path";
// Step 1 Run the scan upwards logic once to find the location of the closest ancestor project.json
// Once this step is run the {InternalScanUpProjectRootKey} is set in the Settings Registry
// to have this scan logic only run once for the supplied registry
// SettingsRegistryInterface::GetType is used to check if a key is set
if (settingsRegistry.GetType(InternalScanUpProjectRootKey) == SettingsRegistryInterface::Type::NoType)
if (settingsRegistry.GetType(InternalScanUpProjectRootKey) == Type::NoType)
{
projectRoot = Internal::ScanUpRootLocator("project.json");
// Set the {InternalScanUpProjectRootKey} to make sure this code path isn't called again for this settings registry
@@ -305,19 +312,129 @@ namespace AZ::SettingsRegistryMergeUtils
}
// Step 2 Check the project-path key
// This is the project path root key, as in passed from command-line or .setreg files.
if (settingsRegistry.Get(projectRoot.Native(), projectRootKey))
// This is the project path root key, as passed from command-line or *.setreg files.
settingsRegistry.Get(projectRoot.Native(), projectRootKey);
return projectRoot;
}
//! The algorithm that is used to find the project cache is as follows
//! 1. The "{BootstrapSettingsRootKey}/project_cache_path" is checked for the path
//! 2. Otherwise append the ProductCacheDirectoryName constant to the <project-path>
static AZ::IO::FixedMaxPath FindProjectCachePath(SettingsRegistryInterface& settingsRegistry, const AZ::IO::FixedMaxPath& projectPath)
{
using FixedValueString = SettingsRegistryInterface::FixedValueString;
constexpr auto projectCachePathKey = FixedValueString(BootstrapSettingsRootKey) + "/project_cache_path";
// Step 1 Check the project-cache-path key
if (AZ::IO::FixedMaxPath projectCachePath; settingsRegistry.Get(projectCachePath.Native(), projectCachePathKey))
{
return projectRoot;
return projectCachePath;
}
// Step 3 Check for a "Cache" directory by scanning upwards from the executable directory
if (auto candidateRoot = Internal::ScanUpRootLocator("Cache");
!candidateRoot.empty() && AZ::IO::SystemFile::IsDirectory(candidateRoot.c_str()))
// Step 2 Append the "Cache" directory to the project-path
return projectPath / Internal::ProductCacheDirectoryName;
}
//! Set the user directory with the provided path or using <project-path>/user as default
static AZ::IO::FixedMaxPath FindProjectUserPath(SettingsRegistryInterface& settingsRegistry,
const AZ::IO::FixedMaxPath& projectPath)
{
using FixedValueString = SettingsRegistryInterface::FixedValueString;
// User: root - same as the @user@ alias, this is the starting path for transient data and log files.
constexpr auto projectUserPathKey = FixedValueString(BootstrapSettingsRootKey) + "/project_user_path";
// Step 1 Check the project-user-path key
if (AZ::IO::FixedMaxPath projectUserPath; settingsRegistry.Get(projectUserPath.Native(), projectUserPathKey))
{
projectRoot = AZStd::move(candidateRoot);
return projectUserPath;
}
// Step 2 Append the "User" directory to the project-path
return projectPath / "user";
}
//! Set the log directory using the settings registry path or using <project-user-path>/log as default
static AZ::IO::FixedMaxPath FindProjectLogPath(SettingsRegistryInterface& settingsRegistry,
const AZ::IO::FixedMaxPath& projectUserPath)
{
using FixedValueString = SettingsRegistryInterface::FixedValueString;
// User: root - same as the @log@ alias, this is the starting path for transient data and log files.
constexpr auto projectLogPathKey = FixedValueString(BootstrapSettingsRootKey) + "/project_log_path";
// Step 1 Check the project-user-path key
if (AZ::IO::FixedMaxPath projectLogPath; settingsRegistry.Get(projectLogPath.Native(), projectLogPathKey))
{
return projectLogPath;
}
// Step 2 Append the "Log" directory to the project-user-path
return projectUserPath / "log";
}
// check for a default write storage path, fall back to the <project-user-path> if not
static AZ::IO::FixedMaxPath FindDevWriteStoragePath(const AZ::IO::FixedMaxPath& projectUserPath)
{
AZStd::optional<AZ::IO::FixedMaxPathString> devWriteStorage = Utils::GetDevWriteStoragePath();
return devWriteStorage.has_value() ? *devWriteStorage : projectUserPath;
}
// check for the project build path, which is a relative path from the project root
// that specifies where the build directory is located
static void SetProjectBuildPath(SettingsRegistryInterface& settingsRegistry,
const AZ::IO::FixedMaxPath& projectPath)
{
if (AZ::IO::FixedMaxPath projectBuildPath; settingsRegistry.Get(projectBuildPath.Native(), ProjectBuildPath))
{
settingsRegistry.Remove(FilePathKey_ProjectBuildPath);
settingsRegistry.Remove(FilePathKey_ProjectConfigurationBinPath);
AZ::IO::FixedMaxPath buildConfigurationPath = (projectPath / projectBuildPath).LexicallyNormal();
if (IO::SystemFile::Exists(buildConfigurationPath.c_str()))
{
settingsRegistry.Set(FilePathKey_ProjectBuildPath, buildConfigurationPath.Native());
}
// Add the specific build configuration paths to the Settings Registry
// First try <project-build-path>/bin/$<CONFIG> and if that path doesn't exist
// try <project-build-path>/bin/$<PLATFORM>/$<CONFIG>
buildConfigurationPath /= "bin";
if (IO::SystemFile::Exists((buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE).c_str()))
{
settingsRegistry.Set(FilePathKey_ProjectConfigurationBinPath,
(buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE).Native());
}
else if (IO::SystemFile::Exists((buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE).c_str()))
{
settingsRegistry.Set(FilePathKey_ProjectConfigurationBinPath,
(buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE).Native());
}
}
}
// Sets the project name within the Settings Registry by looking up the "project_name"
// within the project.json file
static void SetProjectName(SettingsRegistryInterface& settingsRegistry,
const AZ::IO::FixedMaxPath& projectPath)
{
using FixedValueString = SettingsRegistryInterface::FixedValueString;
// Project name - if it was set via merging project.json use that value, otherwise use the project path's folder name.
constexpr auto projectNameKey = FixedValueString(ProjectSettingsRootKey) + "/project_name";
// Read the project name from the project.json file if it exists
if (AZ::IO::FixedMaxPath projectJsonPath = projectPath / "project.json";
AZ::IO::SystemFile::Exists(projectJsonPath.c_str()))
{
settingsRegistry.MergeSettingsFile(projectJsonPath.Native(),
AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey);
}
// If a project name isn't set the default will be set to the final path segment of the project path
if (FixedValueString projectName; !settingsRegistry.Get(projectName, projectNameKey))
{
projectName = projectPath.Filename().Native();
settingsRegistry.Set(projectNameKey, projectName);
}
return projectRoot;
}
AZStd::string_view ConfigParserSettings::DefaultCommentPrefixFilter(AZStd::string_view line)
@@ -397,7 +514,7 @@ namespace AZ::SettingsRegistryMergeUtils
bool MergeSettingsToRegistry_ConfigFile(SettingsRegistryInterface& registry, AZStd::string_view filePath,
const ConfigParserSettings& configParserSettings)
{
auto configPath = FindEngineRoot(registry) / filePath;
auto configPath = FindProjectRoot(registry) / filePath;
IO::FileReader configFile;
bool configFileOpened{};
switch (configParserSettings.m_fileReaderClass)
@@ -542,19 +659,77 @@ namespace AZ::SettingsRegistryMergeUtils
void MergeSettingsToRegistry_AddRuntimeFilePaths(SettingsRegistryInterface& registry)
{
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
// Binary folder
AZ::IO::FixedMaxPath path = AZ::Utils::GetExecutableDirectory();
registry.Set(FilePathKey_BinaryFolder, path.LexicallyNormal().Native());
// Engine root folder - corresponds to the @engroot@ and @engroot@ aliases
AZ::IO::FixedMaxPath engineRoot = FindEngineRoot(registry);
registry.Set(FilePathKey_EngineRootFolder, engineRoot.LexicallyNormal().Native());
// Binary folder - corresponds to the @exefolder@ alias
AZ::IO::FixedMaxPath exePath = AZ::Utils::GetExecutableDirectory();
registry.Set(FilePathKey_BinaryFolder, exePath.LexicallyNormal().Native());
auto projectPathKey = FixedValueString::format("%s/project_path", BootstrapSettingsRootKey);
SettingsRegistryInterface::FixedValueString projectPathValue;
if (registry.Get(projectPathValue, projectPathKey))
// Project path - corresponds to the @projectroot@ alias
// NOTE: We make the project-path in the BootstrapSettingsRootKey absolute first
AZ::IO::FixedMaxPath projectPath = FindProjectRoot(registry);
if (constexpr auto projectPathKey = FixedValueString(BootstrapSettingsRootKey) + "/project_path";
!projectPath.empty())
{
// Cache folder
if (projectPath.IsRelative())
{
if (auto projectAbsPath = AZ::Utils::ConvertToAbsolutePath(projectPath.Native());
projectAbsPath.has_value())
{
projectPath = AZStd::move(*projectAbsPath);
}
}
projectPath = projectPath.LexicallyNormal();
AZ_Warning("SettingsRegistryMergeUtils", AZ::IO::SystemFile::Exists(projectPath.c_str()),
R"(Project path "%s" does not exist. Is the "%.*s" registry setting set to a valid absolute path?)"
, projectPath.c_str(), AZ_STRING_ARG(projectPathKey));
registry.Set(FilePathKey_ProjectPath, projectPath.Native());
}
else
{
AZ_TracePrintf("SettingsRegistryMergeUtils",
R"(Project path isn't set in the Settings Registry at "%.*s".)"
" Project-related filepaths will be set relative to the executable directory\n",
AZ_STRING_ARG(projectPathKey));
registry.Set(FilePathKey_ProjectPath, exePath.Native());
}
// Engine root folder - corresponds to the @engroot@ alias
AZ::IO::FixedMaxPath engineRoot = FindEngineRoot(registry);
if (!engineRoot.empty())
{
if (engineRoot.IsRelative())
{
if (auto engineRootAbsPath = AZ::Utils::ConvertToAbsolutePath(engineRoot.Native());
engineRootAbsPath.has_value())
{
engineRoot = AZStd::move(*engineRootAbsPath);
}
}
engineRoot = engineRoot.LexicallyNormal();
registry.Set(FilePathKey_EngineRootFolder, engineRoot.Native());
}
// Cache folder
AZ::IO::FixedMaxPath projectCachePath = FindProjectCachePath(registry, projectPath).LexicallyNormal();
if (!projectCachePath.empty())
{
if (projectCachePath.IsRelative())
{
if (auto projectCacheAbsPath = AZ::Utils::ConvertToAbsolutePath(projectCachePath.Native());
projectCacheAbsPath.has_value())
{
projectCachePath = AZStd::move(*projectCacheAbsPath);
}
}
projectCachePath = projectCachePath.LexicallyNormal();
registry.Set(FilePathKey_CacheProjectRootFolder, projectCachePath.Native());
// Cache/<asset-platform> folder
// Get the name of the asset platform assigned by the bootstrap. First check for platform version such as "windows_assets"
// and if that's missing just get "assets".
FixedValueString assetPlatform;
@@ -570,124 +745,67 @@ namespace AZ::SettingsRegistryMergeUtils
assetPlatform = AZ::OSPlatformToDefaultAssetPlatform(AZ_TRAIT_OS_PLATFORM_CODENAME);
}
// Project path - corresponds to the @projectroot@ alias
// NOTE: Here we append to engineRoot, but if projectPathValue is absolute then engineRoot is discarded.
path = engineRoot / projectPathValue;
AZ_Warning("SettingsRegistryMergeUtils", AZ::IO::SystemFile::Exists(path.c_str()),
R"(Project path "%s" does not exist. Is the "%.*s" registry setting set to valid absolute path?)"
, path.c_str(), aznumeric_cast<int>(projectPathKey.size()), projectPathKey.data());
AZ::IO::FixedMaxPath normalizedProjectPath = path.LexicallyNormal();
registry.Set(FilePathKey_ProjectPath, normalizedProjectPath.Native());
// Set the user directory with the provided path or using project/user as default
auto projectUserPathKey = FixedValueString::format("%s/project_user_path", BootstrapSettingsRootKey);
AZ::IO::FixedMaxPath projectUserPath;
if (!registry.Get(projectUserPath.Native(), projectUserPathKey))
{
projectUserPath = (normalizedProjectPath / "user").LexicallyNormal();
}
registry.Set(FilePathKey_ProjectUserPath, projectUserPath.Native());
// Set the log directory with the provided path or using project/user/log as default
auto projectLogPathKey = FixedValueString::format("%s/project_log_path", BootstrapSettingsRootKey);
AZ::IO::FixedMaxPath projectLogPath;
if (!registry.Get(projectLogPath.Native(), projectLogPathKey))
{
projectLogPath = (projectUserPath / "log").LexicallyNormal();
}
registry.Set(FilePathKey_ProjectLogPath, projectLogPath.Native());
// check for a default write storage path, fall back to the project's user/ directory if not
AZStd::optional<AZ::IO::FixedMaxPathString> devWriteStorage = Utils::GetDevWriteStoragePath();
registry.Set(FilePathKey_DevWriteStorage, devWriteStorage.has_value()
? devWriteStorage.value()
: projectUserPath.Native());
// Set the project in-memory build path if the ProjectBuildPath key has been supplied
if (AZ::IO::FixedMaxPath projectBuildPath; registry.Get(projectBuildPath.Native(), ProjectBuildPath))
{
registry.Remove(FilePathKey_ProjectBuildPath);
registry.Remove(FilePathKey_ProjectConfigurationBinPath);
AZ::IO::FixedMaxPath buildConfigurationPath = normalizedProjectPath / projectBuildPath;
if (IO::SystemFile::Exists(buildConfigurationPath.c_str()))
{
registry.Set(FilePathKey_ProjectBuildPath, buildConfigurationPath.LexicallyNormal().Native());
}
// Add the specific build configuration paths to the Settings Registry
// First try <project-build-path>/bin/$<CONFIG> and if that path doesn't exist
// try <project-build-path>/bin/$<PLATFORM>/$<CONFIG>
buildConfigurationPath /= "bin";
if (IO::SystemFile::Exists((buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE).c_str()))
{
registry.Set(FilePathKey_ProjectConfigurationBinPath,
(buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE).LexicallyNormal().Native());
}
else if (IO::SystemFile::Exists((buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE).c_str()))
{
registry.Set(FilePathKey_ProjectConfigurationBinPath,
(buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE).LexicallyNormal().Native());
}
}
// Project name - if it was set via merging project.json use that value, otherwise use the project path's folder name.
constexpr auto projectNameKey =
FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey)
+ "/project_name";
// Read the project name from the project.json file if it exists
if (AZ::IO::FixedMaxPath projectJsonPath = normalizedProjectPath / "project.json";
AZ::IO::SystemFile::Exists(projectJsonPath.c_str()))
{
registry.MergeSettingsFile(projectJsonPath.Native(),
AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey);
}
if (FixedValueString projectName; !registry.Get(projectName, projectNameKey))
{
projectName = path.Filename().Native();
registry.Set(projectNameKey, projectName);
}
// Cache folders - sets up various paths in registry for the cache.
// Make sure the asset platform is set before setting these cache paths.
// Make sure the asset platform is set before setting cache path for the asset platform.
if (!assetPlatform.empty())
{
// Cache: project root - no corresponding fileIO alias, but this is where the asset database lives.
// A registry override is accepted using the "project_cache_path" key.
auto projectCacheRootOverrideKey = FixedValueString::format("%s/project_cache_path", BootstrapSettingsRootKey);
// Clear path to make sure that the `project_cache_path` value isn't concatenated to the project path
path.clear();
if (registry.Get(path.Native(), projectCacheRootOverrideKey))
{
registry.Set(FilePathKey_CacheProjectRootFolder, path.LexicallyNormal().Native());
path /= assetPlatform;
registry.Set(FilePathKey_CacheRootFolder, path.LexicallyNormal().Native());
}
else
{
// Cache: root - same as the @products@ alias, this is the starting path for cache files.
path = normalizedProjectPath / "Cache";
registry.Set(FilePathKey_CacheProjectRootFolder, path.LexicallyNormal().Native());
path /= assetPlatform;
registry.Set(FilePathKey_CacheRootFolder, path.LexicallyNormal().Native());
}
registry.Set(FilePathKey_CacheRootFolder, (projectCachePath / assetPlatform).Native());
}
}
else
// User folder
AZ::IO::FixedMaxPath projectUserPath = FindProjectUserPath(registry, projectPath);
if (!projectUserPath.empty())
{
// Set the default ProjectUserPath to the <engine-root>/user directory
registry.Set(FilePathKey_ProjectUserPath, (engineRoot / "user").LexicallyNormal().Native());
AZ_TracePrintf("SettingsRegistryMergeUtils",
R"(Project path isn't set in the Settings Registry at "%.*s". Project-related filepaths will not be set)" "\n",
aznumeric_cast<int>(projectPathKey.size()), projectPathKey.data());
if (projectUserPath.IsRelative())
{
if (auto projectUserAbsPath = AZ::Utils::ConvertToAbsolutePath(projectUserPath.Native());
projectUserAbsPath.has_value())
{
projectUserPath = AZStd::move(*projectUserAbsPath);
}
}
projectUserPath = projectUserPath.LexicallyNormal();
registry.Set(FilePathKey_ProjectUserPath, projectUserPath.Native());
}
// Log folder
if (AZ::IO::FixedMaxPath projectLogPath = FindProjectLogPath(registry, projectUserPath); !projectLogPath.empty())
{
if (projectLogPath.IsRelative())
{
if (auto projectLogAbsPath = AZ::Utils::ConvertToAbsolutePath(projectLogPath.Native()))
{
projectLogPath = AZStd::move(*projectLogAbsPath);
}
}
projectLogPath = projectLogPath.LexicallyNormal();
registry.Set(FilePathKey_ProjectLogPath, projectLogPath.Native());
}
// Developer Write Storage folder
if (AZ::IO::FixedMaxPath devWriteStoragePath = FindDevWriteStoragePath(projectUserPath); !devWriteStoragePath.empty())
{
if (devWriteStoragePath.IsRelative())
{
if (auto devWriteStorageAbsPath = AZ::Utils::ConvertToAbsolutePath(devWriteStoragePath.Native()))
{
devWriteStoragePath = AZStd::move(*devWriteStorageAbsPath);
}
}
devWriteStoragePath = devWriteStoragePath.LexicallyNormal();
registry.Set(FilePathKey_DevWriteStorage, devWriteStoragePath.Native());
}
// Set the project in-memory build path if the ProjectBuildPath key has been supplied
SetProjectBuildPath(registry, projectPath);
// Set the project name using the "project_name" key
SetProjectName(registry, projectPath);
#if !AZ_TRAIT_OS_IS_HOST_OS_PLATFORM
// Setup the cache, user, and log paths to platform specific locations when running on non-host platforms
path = engineRoot;
if (AZStd::optional<AZ::IO::FixedMaxPathString> nonHostCacheRoot = Utils::GetDefaultAppRootPath();
nonHostCacheRoot)
{
@@ -696,25 +814,25 @@ namespace AZ::SettingsRegistryMergeUtils
}
else
{
registry.Set(FilePathKey_CacheProjectRootFolder, path.LexicallyNormal().Native());
registry.Set(FilePathKey_CacheRootFolder, path.LexicallyNormal().Native());
registry.Set(FilePathKey_CacheProjectRootFolder, projectPath.Native());
registry.Set(FilePathKey_CacheRootFolder, projectPath.Native());
}
if (AZStd::optional<AZ::IO::FixedMaxPathString> devWriteStorage = Utils::GetDevWriteStoragePath();
devWriteStorage)
{
const AZ::IO::FixedMaxPath devWriteStoragePath(*devWriteStorage);
registry.Set(FilePathKey_DevWriteStorage, devWriteStoragePath.LexicallyNormal().Native());
registry.Set(FilePathKey_ProjectUserPath, (devWriteStoragePath / "user").LexicallyNormal().Native());
registry.Set(FilePathKey_ProjectLogPath, (devWriteStoragePath / "user/log").LexicallyNormal().Native());
const auto devWriteStoragePath = AZ::IO::PathView(*devWriteStorage).LexicallyNormal();
registry.Set(FilePathKey_DevWriteStorage, devWriteStoragePath.Native());
registry.Set(FilePathKey_ProjectUserPath, (devWriteStoragePath / "user").Native());
registry.Set(FilePathKey_ProjectLogPath, (devWriteStoragePath / "user" / "log").Native());
}
else
{
registry.Set(FilePathKey_DevWriteStorage, path.LexicallyNormal().Native());
registry.Set(FilePathKey_ProjectUserPath, (path / "user").LexicallyNormal().Native());
registry.Set(FilePathKey_ProjectLogPath, (path / "user/log").LexicallyNormal().Native());
}
#endif // AZ_TRAIT_OS_IS_HOST_OS_PLATFORM
registry.Set(FilePathKey_DevWriteStorage, projectPath.Native());
registry.Set(FilePathKey_ProjectUserPath, (projectPath / "user").Native());
registry.Set(FilePathKey_ProjectLogPath, (projectPath / "user" / "log").Native());
}
#endif // AZ_TRAIT_OS_IS_HOST_OS_PLATFORM
}
void MergeSettingsToRegistry_TargetBuildDependencyRegistry(SettingsRegistryInterface& registry, const AZStd::string_view platform,
const SettingsRegistryInterface::Specializations& specializations, AZStd::vector<char>* scratchBuffer)
@@ -87,9 +87,9 @@ namespace AZ::SettingsRegistryMergeUtils
AZ::IO::FixedMaxPath FindEngineRoot(SettingsRegistryInterface& settingsRegistry);
//! The algorithm that is used to find the project root is as follows
//! 1. The first time this function is it performs a upward scan for a project.json file from
//! the executable directory and if found stores that path to an internal key.
//! In the same step it injects the path into the front of list of command line parameters
//! 1. The first time this function runs it performs an upward scan for a "project.json" file from
//! the executable directory and stores that path into an internal key.
//! In the same step it injects the path into the back of the command line parameters
//! using the --regset="{BootstrapSettingsRootKey}/project_path=<path>" value
//! 2. Next the "{BootstrapSettingsRootKey}/project_path" is checked to see if it has a project path set
//!
+1 -1
View File
@@ -59,7 +59,7 @@ namespace AZ::Utils
{
// Fix the size value of the fixed string by calculating the c-string length using char traits
absolutePath.resize_no_construct(AZStd::char_traits<char>::length(absolutePath.data()));
return srcPath;
return absolutePath;
}
return AZStd::nullopt;
@@ -635,6 +635,7 @@ namespace AZ
size_t longestMatch = 0;
size_t bufStringLength = inBuffer.size();
AZStd::string_view longestAlias;
AZStd::string_view longestResolvedAlias;
for (const auto& [alias, resolvedAlias] : m_aliases)
{
@@ -653,6 +654,7 @@ namespace AZ
{
longestMatch = resolvedAlias.size();
longestAlias = alias;
longestResolvedAlias = resolvedAlias;
}
}
}
@@ -661,7 +663,10 @@ namespace AZ
// rearrange the buffer to have
// [alias][old path]
size_t aliasSize = longestAlias.size();
size_t charsToAbsorb = longestMatch;
// If the resolved alias ends in a path separator, do not consume it.
const bool resolvedAliasEndsInPathSeparator = (longestResolvedAlias.ends_with(AZ::IO::PosixPathSeparator) ||
longestResolvedAlias.ends_with(AZ::IO::WindowsPathSeparator));
const size_t charsToAbsorb = resolvedAliasEndsInPathSeparator ? longestMatch - 1 : longestMatch;
size_t remainingData = bufStringLength - charsToAbsorb;
size_t finalStringSize = aliasSize + remainingData;
if (finalStringSize >= outBufferLength)
@@ -27,28 +27,4 @@ namespace AzFramework
;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputChannelId::GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::Crc32& InputChannelId::GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputChannelId::operator==(const InputChannelId& other) const
{
return (m_crc32 == other.m_crc32);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputChannelId::operator!=(const InputChannelId& other) const
{
return !(*this == other);
}
} // namespace AzFramework
@@ -39,53 +39,58 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Constructor
//! \param[in] name Name of the input channel (will be ignored if exceeds MAX_NAME_LENGTH)
//! \param[in] name Name of the input channel (will be truncated if exceeds MAX_NAME_LENGTH)
explicit constexpr InputChannelId(AZStd::string_view name = "")
: m_name(name)
, m_crc32(name)
: m_name(name.substr(0, MAX_NAME_LENGTH))
, m_crc32(name.substr(0, MAX_NAME_LENGTH))
{
}
constexpr InputChannelId(const InputChannelId& other) = default;
constexpr InputChannelId(InputChannelId&& other) = default;
constexpr InputChannelId& operator=(const InputChannelId& other)
{
m_name = other.m_name;
m_crc32 = other.m_crc32;
return *this;
}
constexpr InputChannelId& operator=(InputChannelId&& other)
{
m_name = AZStd::move(other.m_name);
m_crc32 = AZStd::move(other.m_crc32);
other.m_crc32 = 0;
return *this;
}
////////////////////////////////////////////////////////////////////////////////////////////
// Default copying and moving
AZ_DEFAULT_COPY_MOVE(InputChannelId);
////////////////////////////////////////////////////////////////////////////////////////////
//! Default destructor
~InputChannelId() = default;
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input channel's name
//! \return Name of the input channel
const char* GetName() const;
constexpr const char* GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the crc32 of the input channel's name
//! \return crc32 of the input channel name
const AZ::Crc32& GetNameCrc32() const;
constexpr const AZ::Crc32& GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
///@{
//! Equality comparison operator
//! \param[in] other Another instance of the class to compare for equality
bool operator==(const InputChannelId& other) const;
bool operator!=(const InputChannelId& other) const;
///@}
constexpr bool operator==(const InputChannelId& other) const
{
return m_crc32 == other.m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Inequality comparison operator
//! \param[in] other Another instance of the class to compare for inequality
constexpr bool operator!=(const InputChannelId& other) const
{
return !(*this == other);
}
private:
////////////////////////////////////////////////////////////////////////////////////////////
// Variables
AZStd::fixed_string<MAX_NAME_LENGTH> m_name; //!< Name of the input channel
AZ::Crc32 m_crc32; //!< Crc32 of the input channel
AZ::Crc32 m_crc32; //!< Crc32 of the input channel name
};
} // namespace AzFramework
@@ -14,14 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputDeviceGamepad::Name("gamepad");
const InputDeviceId InputDeviceGamepad::IdForIndex0(Name, 0);
const InputDeviceId InputDeviceGamepad::IdForIndex1(Name, 1);
const InputDeviceId InputDeviceGamepad::IdForIndex2(Name, 2);
const InputDeviceId InputDeviceGamepad::IdForIndex3(Name, 3);
const InputDeviceId InputDeviceGamepad::IdForIndexN(AZ::u32 n) { return InputDeviceId(Name, n); }
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceGamepad::IsGamepadDevice(const InputDeviceId& inputDeviceId)
{
@@ -32,16 +32,16 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The name used to identify any game-pad input device
static const char* Name;
static constexpr inline const char* Name{"gamepad"};
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify a game-pad input device with a specific index
///@{
static const InputDeviceId IdForIndex0;
static const InputDeviceId IdForIndex1;
static const InputDeviceId IdForIndex2;
static const InputDeviceId IdForIndex3;
static const InputDeviceId IdForIndexN(AZ::u32 n);
static constexpr inline InputDeviceId IdForIndex0{Name, 0};
static constexpr inline InputDeviceId IdForIndex1{Name, 1};
static constexpr inline InputDeviceId IdForIndex2{Name, 2};
static constexpr inline InputDeviceId IdForIndex3{Name, 3};
static constexpr inline InputDeviceId IdForIndexN(AZ::u32 n) { return InputDeviceId(Name, n); }
///@}
////////////////////////////////////////////////////////////////////////////////////////////
@@ -29,71 +29,4 @@ namespace AzFramework
;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId::InputDeviceId(const char* name, AZ::u32 index)
: m_crc32(name)
, m_index(index)
{
memset(m_name, 0, AZ_ARRAY_SIZE(m_name));
azstrncpy(m_name, NAME_BUFFER_SIZE, name, MAX_NAME_LENGTH);
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId::InputDeviceId(const InputDeviceId& other)
: m_crc32(other.m_crc32)
, m_index(other.m_index)
{
memset(m_name, 0, AZ_ARRAY_SIZE(m_name));
azstrcpy(m_name, NAME_BUFFER_SIZE, other.m_name);
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId& InputDeviceId::operator=(const InputDeviceId& other)
{
azstrcpy(m_name, NAME_BUFFER_SIZE, other.m_name);
m_crc32 = other.m_crc32;
m_index = other.m_index;
return *this;
}
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputDeviceId::GetName() const
{
return m_name;
}
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::Crc32& InputDeviceId::GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////////
AZ::u32 InputDeviceId::GetIndex() const
{
return m_index;
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator==(const InputDeviceId& other) const
{
return (m_crc32 == other.m_crc32) && (m_index == other.m_index);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator!=(const InputDeviceId& other) const
{
return !(*this == other);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator<(const InputDeviceId& other) const
{
if (m_index == other.m_index)
{
return m_crc32 < other.m_crc32;
}
return m_index < other.m_index;
}
} // namespace AzFramework
@@ -11,6 +11,7 @@
#include <AzCore/Math/Crc.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/std/hash.h>
#include <AzCore/std/string/fixed_string.h>
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
@@ -22,8 +23,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
// Constants
static const int NAME_BUFFER_SIZE = 64;
static const int MAX_NAME_LENGTH = NAME_BUFFER_SIZE - 1;
static constexpr int MAX_NAME_LENGTH = 64;
////////////////////////////////////////////////////////////////////////////////////////////
// Allocator
@@ -41,17 +41,16 @@ namespace AzFramework
//! Constructor
//! \param[in] name Name of the input device (will be truncated if exceeds MAX_NAME_LENGTH)
//! \param[in] index Index of the input device (optional)
explicit InputDeviceId(const char* name, AZ::u32 index = 0);
explicit constexpr InputDeviceId(AZStd::string_view name, AZ::u32 index = 0)
: m_name(name.substr(0, MAX_NAME_LENGTH))
, m_crc32(name.substr(0, MAX_NAME_LENGTH))
, m_index(index)
{
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Copy constructor
//! \param[in] other Another instance of the class to copy from
InputDeviceId(const InputDeviceId& other);
////////////////////////////////////////////////////////////////////////////////////////////
//! Copy assignment operator
//! \param[in] other Another instance of the class to copy from
InputDeviceId& operator=(const InputDeviceId& other);
// Default copying and moving
AZ_DEFAULT_COPY_MOVE(InputDeviceId);
////////////////////////////////////////////////////////////////////////////////////////////
//! Default destructor
@@ -60,12 +59,18 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input device's name
//! \return Name of the input device
const char* GetName() const;
constexpr const char* GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the crc32 of the input device's name
//! \return crc32 of the input device name
const AZ::Crc32& GetNameCrc32() const;
constexpr const AZ::Crc32& GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input device's index. Used for differentiating between multiple instances
@@ -75,27 +80,45 @@ namespace AzFramework
//! at startup using indicies 0->3. As gamepads connect/disconnect at runtime we assign the
//! appropriate (system dependent) local user id (see InputDevice::GetAssignedLocalUserId).
//! \return Index of the input device
AZ::u32 GetIndex() const;
constexpr AZ::u32 GetIndex() const
{
return m_index;
}
////////////////////////////////////////////////////////////////////////////////////////////
///@{
//! Equality comparison operator
//! \param[in] other Another instance of the class to compare for equality
bool operator==(const InputDeviceId& other) const;
bool operator!=(const InputDeviceId& other) const;
///@}
constexpr bool operator==(const InputDeviceId& other) const
{
return (m_crc32 == other.m_crc32) && (m_index == other.m_index);
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Inequality comparison operator
//! \param[in] other Another instance of the class to compare for inequality
constexpr bool operator!=(const InputDeviceId& other) const
{
return !(*this == other);
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Less than comparison operator
//! \param[in] other Another instance of the class to compare
bool operator<(const InputDeviceId& other) const;
constexpr bool operator<(const InputDeviceId& other) const
{
if (m_index == other.m_index)
{
return m_crc32 < other.m_crc32;
}
return m_index < other.m_index;
}
private:
////////////////////////////////////////////////////////////////////////////////////////////
// Variables
char m_name[NAME_BUFFER_SIZE]; //!< Name of the input device
AZ::Crc32 m_crc32; //!< Crc32 of the input device
AZ::u32 m_index; //!< Index of the input device
AZStd::fixed_string<MAX_NAME_LENGTH> m_name; //!< Name of the input device
AZ::Crc32 m_crc32; //!< Crc32 of the input device name
AZ::u32 m_index; //!< Index of the input device
};
} // namespace AzFramework
@@ -15,9 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceKeyboard::Id("keyboard");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceKeyboard::IsKeyboardDevice(const InputDeviceId& inputDeviceId)
{
@@ -33,7 +33,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary physical keyboard input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"keyboard"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a physical keyboard (regardless of index)
@@ -14,9 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceMotion::Id("motion");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceMotion::IsMotionDevice(const InputDeviceId& inputDeviceId)
{
@@ -28,7 +28,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary motion input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"motion"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a motion device (regardless of index)
@@ -15,18 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateDefault = 60;
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateQueueAll = std::numeric_limits<AZ::u32>::max();
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateAccumulateAll = 0;
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceMouse::Id("mouse");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceMouse::IsMouseDevice(const InputDeviceId& inputDeviceId)
{
@@ -31,23 +31,23 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Default sample rate for raw mouse movement events that aims to strike a balance between
//! responsiveness and performance.
static const AZ::u32 MovementSampleRateDefault;
static constexpr inline AZ::u32 MovementSampleRateDefault{60};
////////////////////////////////////////////////////////////////////////////////////////////
//! Sample rate for raw mouse movement that will cause all events received in the same frame
//! to be queued and dispatched as individual events. This results in maximum responsiveness
//! but may potentially impact performance depending how many events happen over each frame.
static const AZ::u32 MovementSampleRateQueueAll;
static constexpr inline AZ::u32 MovementSampleRateQueueAll{std::numeric_limits<AZ::u32>::max()};
////////////////////////////////////////////////////////////////////////////////////////////
//! Sample rate for raw mouse movement that will cause all events received in the same frame
//! to be accumulated and dispatched as a single event. Optimal for performance, but results
//! in sluggish/unresponsive mouse movement, especially when running at low frame rates.
static const AZ::u32 MovementSampleRateAccumulateAll;
static constexpr inline AZ::u32 MovementSampleRateAccumulateAll{0};
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary mouse input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"mouse"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a mouse (regardless of index)
@@ -15,9 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceTouch::Id("touch");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceTouch::IsTouchDevice(const InputDeviceId& inputDeviceId)
{
@@ -25,7 +25,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary touch input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"touch"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a touch device (regardless of index)
@@ -14,9 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceVirtualKeyboard::Id("virtual_keyboard");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceVirtualKeyboard::IsVirtualKeyboardDevice(const InputDeviceId& inputDeviceId)
{
@@ -25,7 +25,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary virtual keyboard input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"virtual_keyboard"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a virtual keyboard (regardless of index)
@@ -30,12 +30,22 @@ namespace AzFramework
//! Called when the root spawnable has been assigned a new value. This may be called several times without a call to release
//! in between.
//! @note: The callback is not queued but immediately called from a random thread. This is done because this callback is typically
//! used before entities are spawned and if it's queued then the entities spawn before this callback is called.
//! @param rootSpawnable The new root spawnable that was assigned.
//! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned.
virtual void OnRootSpawnableAssigned([[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable,
[[maybe_unused]] uint32_t generation) {}
//! Called when the root spawnable has completed spawning of entities. This may be called several times without a call to release
//! in between.
//! @note: This callback is queued and will be called with a delay and from the main thread.
//! @param rootSpawnable The new root spawnable that was used to spawn entities from.
//! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned.
virtual void OnRootSpawnableReady(
[[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable, [[maybe_unused]] uint32_t generation) {}
//! Called when the root spawnable has Released. This will only be called if there's no root spawnable assigned to take the
//! place of the original root spawnable.
//! Note: This callback is queued and will be called with a delay and from the main thread.
//! @param generation The generation of the root spawnable that was released.
virtual void OnRootSpawnableReleased([[maybe_unused]] uint32_t generation) {}
};
@@ -6,12 +6,473 @@
*
*/
#include <AzCore/Asset/AssetSerializer.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/numeric.h>
#include <AzCore/std/sort.h>
#include <AzCore/std/typetraits/typetraits.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AzFramework
{
//
// EntityAlias
//
bool Spawnable::EntityAlias::HasLowerIndex(const EntityAlias& other) const
{
return m_sourceIndex == other.m_sourceIndex ?
m_aliasType < other.m_aliasType :
m_sourceIndex < other.m_sourceIndex;
}
//
// EntityAliasVisitorBase
//
bool Spawnable::EntityAliasVisitorBase::IsValid(const EntityAliasList* aliases) const
{
return aliases != nullptr;
}
bool Spawnable::EntityAliasVisitorBase::HasAliases(const EntityAliasList* aliases) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return !aliases->empty();
}
bool Spawnable::EntityAliasVisitorBase::AreAllSpawnablesReady(const EntityAliasList* aliases) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (const EntityAlias& alias : *aliases)
{
if (!alias.m_queueLoad ||
alias.m_aliasType == Spawnable::EntityAliasType::Original ||
alias.m_aliasType == Spawnable::EntityAliasType::Disable)
{
continue;
}
if (!alias.m_spawnable.IsReady() && !alias.m_spawnable.IsError())
{
return false;
}
}
return true;
}
auto Spawnable::EntityAliasVisitorBase::begin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cbegin();
}
auto Spawnable::EntityAliasVisitorBase::end(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cend();
}
auto Spawnable::EntityAliasVisitorBase::cbegin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cbegin();
}
auto Spawnable::EntityAliasVisitorBase::cend(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cend();
}
void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables(
const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZStd::unordered_set<AZ::Data::AssetId> spawnableIds;
for (const Spawnable::EntityAlias& alias : *aliases)
{
// If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on.
if (alias.m_spawnable.GetId().IsValid())
{
auto it = spawnableIds.find(alias.m_spawnable.GetId());
if (it == spawnableIds.end())
{
callback(alias.m_spawnable);
spawnableIds.emplace(alias.m_spawnable.GetId());
}
}
}
}
void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables(
const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZStd::unordered_set<AZ::Data::AssetId> spawnableIds;
for (const Spawnable::EntityAlias& alias : *aliases)
{
// If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on.
if (alias.m_tag == tag && alias.m_spawnable.GetId().IsValid())
{
auto it = spawnableIds.find(alias.m_spawnable.GetId());
if (it == spawnableIds.end())
{
callback(alias.m_spawnable);
spawnableIds.emplace(alias.m_spawnable.GetId());
}
}
}
}
//
// EntityAliasVisitor
//
Spawnable::EntityAliasVisitor::EntityAliasVisitor(Spawnable& owner, EntityAliasList* entityAliasList)
: m_owner(owner)
, m_entityAliasList(entityAliasList)
{
}
Spawnable::EntityAliasVisitor::~EntityAliasVisitor()
{
if (IsValid())
{
Optimize();
AZ_Assert(
m_owner.m_shareState == ShareState::ReadWrite, "Attempting to unlock a spawnable that's not in the locked state (%i).",
m_owner.m_shareState.load());
m_owner.m_shareState = ShareState::NotShared;
}
}
Spawnable::EntityAliasVisitor::EntityAliasVisitor(EntityAliasVisitor&& rhs)
: m_owner(rhs.m_owner)
, m_entityAliasList(rhs.m_entityAliasList)
{
m_dirty = rhs.m_dirty;
rhs.m_entityAliasList = nullptr;
rhs.m_dirty = false;
}
auto Spawnable::EntityAliasVisitor::operator=(EntityAliasVisitor&& rhs) -> EntityAliasVisitor&
{
if (this != &rhs)
{
this->~EntityAliasVisitor();
new(this) EntityAliasVisitor(AZStd::move(rhs));
}
return *this;
}
bool Spawnable::EntityAliasVisitor::IsValid() const
{
return EntityAliasVisitorBase::IsValid(m_entityAliasList);
}
bool Spawnable::EntityAliasVisitor::HasAliases() const
{
return EntityAliasVisitorBase::HasAliases(m_entityAliasList);
}
bool Spawnable::EntityAliasVisitor::AreAllSpawnablesReady() const
{
return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::begin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::begin(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::end() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::end(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::cbegin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cbegin(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::cend() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cend(m_entityAliasList);
}
void Spawnable::EntityAliasVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback);
}
void Spawnable::EntityAliasVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback);
}
void Spawnable::EntityAliasVisitor::AddAlias(
AZ::Data::Asset<Spawnable> targetSpawnable,
AZ::Crc32 tag,
uint32_t sourceIndex,
uint32_t targetIndex,
Spawnable::EntityAliasType aliasType,
bool queueLoad)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZ_Assert(sourceIndex < m_owner.GetEntities().size(), "Invalid source index (%i) for entity alias", sourceIndex);
if (targetSpawnable.IsReady())
{
AZ_Assert(
targetIndex < targetSpawnable->GetEntities().size(), "Invalid target index (%i) for entity alias '%s'", targetIndex,
targetSpawnable.GetHint().c_str());
}
m_entityAliasList->push_back(Spawnable::EntityAlias{ targetSpawnable, tag, sourceIndex, targetIndex, aliasType, queueLoad });
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
if (alias.m_queueLoad &&
alias.m_aliasType != Spawnable::EntityAliasType::Original &&
alias.m_aliasType != Spawnable::EntityAliasType::Disable &&
!alias.m_spawnable.IsLoading() &&
!alias.m_spawnable.IsReady() &&
!alias.m_spawnable.IsError())
{
callback(alias.m_spawnable);
}
}
}
void Spawnable::EntityAliasVisitor::UpdateAliases(const UpdateCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
AZ::Data::Asset<Spawnable> targetSpawnable(alias.m_spawnable);
callback(alias.m_aliasType, alias.m_queueLoad, targetSpawnable, alias.m_tag, alias.m_sourceIndex, alias.m_targetIndex);
}
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::UpdateAliases(AZ::Crc32 tag, const UpdateCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
if (alias.m_tag == tag)
{
AZ::Data::Asset<Spawnable> targetSpawnable(alias.m_spawnable);
callback(alias.m_aliasType, alias.m_queueLoad, targetSpawnable, alias.m_tag, alias.m_sourceIndex, alias.m_targetIndex);
m_dirty = true;
}
}
}
void Spawnable::EntityAliasVisitor::UpdateAliasType(uint32_t index, Spawnable::EntityAliasType newType)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZ_Assert(
index < m_entityAliasList->size(), "Unable to update entity alias at index %i as there are only %zu aliases in spawnable.",
index, m_entityAliasList->size());
(*m_entityAliasList)[index].m_aliasType = newType;
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::Optimize()
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
if (m_dirty)
{
AZStd::stable_sort(
m_entityAliasList->begin(), m_entityAliasList->end(),
[](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs)
{
// Sort by source index from smallest to largest so during spawning the entities can be iterated linearly over.
// If the source index is the same then sort by alias type so the next steps can optimize away superfluous steps.
return lhs.HasLowerIndex(rhs);
});
// Remove aliases that are not going to have any practical effect and insert aliases where needed to simplify the spawning.
// This is done at runtime rather than at build time because the above ebus allows other systems to make adjustments to the
// aliases, for instance Networking can decide to disable certain aliases when running on a client. This in turn also requires
// the aliases to be in their recorded order during building as the ebus handlers may depend on that order to determine what
// entities need to be updated.
uint32_t previousIndex = AZStd::numeric_limits<uint32_t>::max();
Spawnable::EntityAliasType previousType =
static_cast<Spawnable::EntityAliasType>(AZStd::numeric_limits<AZStd::underlying_type_t<Spawnable::EntityAliasType>>::max());
Spawnable::EntityAlias* it = m_entityAliasList->begin();
Spawnable::EntityAlias* end = m_entityAliasList->end();
while (it < end)
{
// If there's a switch to a new source index and the previous index only had an original it can
// be removed.
if (previousType == Spawnable::EntityAliasType::Original && previousIndex != it->m_sourceIndex)
{
it = m_entityAliasList->erase(it - 1);
end = m_entityAliasList->end();
if (it == end)
{
break;
}
}
switch (it->m_aliasType)
{
case Spawnable::EntityAliasType::Original:
[[fallthrough]];
case Spawnable::EntityAliasType::Disable:
[[fallthrough]];
case Spawnable::EntityAliasType::Replace:
// If the previous entry was a disabled, original or replace alias then remove it as it will be overwritten by the
// current entry.
if (previousIndex == it->m_sourceIndex &&
(previousType == Spawnable::EntityAliasType::Original ||
previousType == Spawnable::EntityAliasType::Disable ||
previousType == Spawnable::EntityAliasType::Replace))
{
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
// Erase instead of a swap-and-pop in order to preserver the order.
it = m_entityAliasList->erase(it - 1) + 1;
end = m_entityAliasList->end();
}
else
{
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
++it;
}
break;
case Spawnable::EntityAliasType::Additional:
[[fallthrough]];
case Spawnable::EntityAliasType::Merge:
// If this is the first entry for this index then insert an original in front of it so the spawnable entity manager
// doesn't have to check for the case there's a merge and/or addition without an entity to extend.
if (previousIndex != it->m_sourceIndex)
{
Spawnable::EntityAlias insert;
// No load, as the asset is already loaded.
insert.m_spawnable = AZ::Data::Asset<Spawnable>({}, azrtti_typeid<Spawnable>());
insert.m_sourceIndex = it->m_sourceIndex;
insert.m_targetIndex = it->m_sourceIndex; // Source index as the original entry for this slot is added.
insert.m_aliasType = Spawnable::EntityAliasType::Original;
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
// Insert to maintain the order.
it = m_entityAliasList->insert(it, AZStd::move(insert));
it += 2;
end = m_entityAliasList->end();
}
else
{
previousType = it->m_aliasType;
++it;
}
break;
default:
AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", it->m_aliasType);
break;
}
}
// Check if the last entry is an "Original" in which case it can be removed.
if (!m_entityAliasList->empty() && m_entityAliasList->back().m_aliasType == Spawnable::EntityAliasType::Original)
{
m_entityAliasList->pop_back();
}
// Reclaim memory because after this point the aliases will not change anymore.
m_entityAliasList->shrink_to_fit();
m_dirty = false;
}
}
//
// EntityAliasConstVisitor
//
Spawnable::EntityAliasConstVisitor::EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList)
: m_owner(owner)
, m_entityAliasList(entityAliasList)
{
}
Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor()
{
if (IsValid())
{
AZ_Assert(
m_owner.m_shareState <= ShareState::Read, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).",
m_owner.m_shareState.load());
m_owner.m_shareState++;
}
}
bool Spawnable::EntityAliasConstVisitor::IsValid() const
{
return EntityAliasVisitorBase::IsValid(m_entityAliasList);
}
bool Spawnable::EntityAliasConstVisitor::HasAliases() const
{
return EntityAliasVisitorBase::HasAliases(m_entityAliasList);
}
bool Spawnable::EntityAliasConstVisitor::AreAllSpawnablesReady() const
{
return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::begin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::begin(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::end() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::end(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::cbegin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cbegin(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::cend() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cend(m_entityAliasList);
}
void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback);
}
void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback);
}
//
// Spawnable
//
Spawnable::Spawnable(const AZ::Data::AssetId& id, AssetStatus status)
: AZ::Data::AssetData(id, status)
{
@@ -27,6 +488,33 @@ namespace AzFramework
return m_entities;
}
auto Spawnable::TryGetAliasesConst() const -> EntityAliasConstVisitor
{
int32_t expected = ShareState::NotShared;
do
{
// Try to set the lock to a negative number to indicate a shared read.
if (m_shareState.compare_exchange_strong(expected, expected - 1))
{
return EntityAliasConstVisitor(*this, &m_entityAliases);
}
// as long as the value is negative or not shared then keep trying to get a shared read lock.
} while (expected <= 0);
return EntityAliasConstVisitor(*this, nullptr);
}
auto Spawnable::TryGetAliases() const -> EntityAliasConstVisitor
{
return TryGetAliasesConst();
}
auto Spawnable::TryGetAliases() -> EntityAliasVisitor
{
int32_t expected = ShareState::NotShared;
return m_shareState.compare_exchange_strong(expected, ShareState::ReadWrite) ? EntityAliasVisitor(*this, &m_entityAliases)
: EntityAliasVisitor(*this, nullptr);
}
bool Spawnable::IsEmpty() const
{
return m_entities.empty();
@@ -44,11 +532,29 @@ namespace AzFramework
void Spawnable::Reflect(AZ::ReflectContext* context)
{
EntityAlias::Reflect(context);
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(1)
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(2)
->Field("Meta data", &Spawnable::m_metaData)
->Field("Entity aliases", &Spawnable::m_entityAliases)
->Field("Entities", &Spawnable::m_entities);
}
}
void Spawnable::EntityAlias::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<Spawnable::EntityAlias>()
->Version(1)
->Field("Spawnable", &EntityAlias::m_spawnable)
->Field("Tag", &EntityAlias::m_tag)
->Field("Source Index", &EntityAlias::m_sourceIndex)
->Field("Target Index", &EntityAlias::m_targetIndex)
->Field("Alias Type", &EntityAlias::m_aliasType)
->Field("Queue Load", &EntityAlias::m_queueLoad);
}
}
} // namespace AzFramework
@@ -11,6 +11,7 @@
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/parallel/atomic.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Spawnable/SpawnableMetaData.h>
@@ -29,7 +30,148 @@ namespace AzFramework
AZ_CLASS_ALLOCATOR(Spawnable, AZ::SystemAllocator, 0);
AZ_RTTI(AzFramework::Spawnable, "{855E3021-D305-4845-B284-20C3F7FDF16B}", AZ::Data::AssetData);
// The order is important for sorting in the SpawnableAssetHandler.
enum class EntityAliasType : uint8_t
{
Original, //!< The original entity is spawned.
Disable, //!< No entity will be spawned.
Replace, //!< The entity alias is spawned instead of the original.
Additional, //!< The original entity is spawned as well as the alias. The alias will get a new entity id.
Merge //!< The original entity is spawned and the components of the alias are added. The caller is responsible for
//!< maintaining a valid component list.
};
enum ShareState : int32_t
{
Read = -1,
NotShared = 0,
ReadWrite = 1
};
//! An entity alias redirects the spawning of an entity to another entity, possibly in another spawnable.
struct EntityAlias
{
AZ_CLASS_ALLOCATOR(EntityAlias, AZ::SystemAllocator, 0);
AZ_TYPE_INFO(AzFramework::Spawnable::EntityAlias, "{C8D0C5BC-1F0B-4572-98C1-73B2CA8C9356}");
bool HasLowerIndex(const EntityAlias& other) const;
AZ::Data::Asset<Spawnable> m_spawnable; //!< The spawnable containing the target entity to spawn.
uint32_t m_tag{ 0 }; //!< A unique tag to identify this alias with.
uint32_t m_sourceIndex{ 0 }; //!< The index of the entity in the original spawnable that will be replaced.
uint32_t m_targetIndex{ 0 }; //!< The index of the entity in the target spawnable that will be used to replace the original.
EntityAliasType m_aliasType{ EntityAliasType::Original }; //!< The kind of replacement.
bool m_queueLoad{ false }; //!< Whether or not to automatically queue the spawnable for loading.
static void Reflect(AZ::ReflectContext* context);
};
using EntityList = AZStd::vector<AZStd::unique_ptr<AZ::Entity>>;
using EntityAliasList = AZStd::vector<EntityAlias>;
private:
class EntityAliasVisitorBase
{
protected:
bool IsValid(const EntityAliasList* aliases) const;
bool HasAliases(const EntityAliasList* aliases) const;
bool AreAllSpawnablesReady(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator begin(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator end(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator cbegin(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator cend(const EntityAliasList* aliases) const;
using ListTargetSpawanblesCallback = AZStd::function<void(const AZ::Data::Asset<Spawnable>& targetSpawnable)>;
void ListTargetSpawnables(const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
};
public:
class EntityAliasVisitor final : public EntityAliasVisitorBase
{
public:
EntityAliasVisitor(Spawnable& owner, EntityAliasList* m_entityAliasList);
~EntityAliasVisitor();
EntityAliasVisitor(EntityAliasVisitor&& rhs);
EntityAliasVisitor& operator=(EntityAliasVisitor&& rhs);
EntityAliasVisitor(const EntityAliasVisitor& rhs) = delete;
EntityAliasVisitor& operator=(const EntityAliasVisitor& rhs) = delete;
//! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions.
bool IsValid() const;
bool HasAliases() const;
bool AreAllSpawnablesReady() const;
// Modification of aliases is limited to specific changes that can only be done through the available modification functions.
// For this reason access through iterators is limited to unmodifiable constant iterators.
EntityAliasList::const_iterator begin() const;
EntityAliasList::const_iterator end() const;
EntityAliasList::const_iterator cbegin() const;
EntityAliasList::const_iterator cend() const;
void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
void AddAlias(
AZ::Data::Asset<Spawnable> targetSpawnable,
AZ::Crc32 tag,
uint32_t sourceIndex,
uint32_t targetIndex,
Spawnable::EntityAliasType aliasType,
bool queueLoad);
using ListSpawnablesRequiringLoadCallback = AZStd::function<void(AZ::Data::Asset<Spawnable>& spawnablePendingLoad)>;
void ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback);
using UpdateCallback = AZStd::function<void(
Spawnable::EntityAliasType& aliasType,
bool& queueLoad,
const AZ::Data::Asset<Spawnable>& aliasedSpawnable,
const AZ::Crc32 tag,
const uint32_t sourceIndex,
const uint32_t targetIndex)>;
void UpdateAliases(const UpdateCallback& callback);
void UpdateAliases(AZ::Crc32 tag, const UpdateCallback& callback);
void UpdateAliasType(uint32_t index, Spawnable::EntityAliasType newType);
void Optimize();
private:
Spawnable& m_owner;
EntityAliasList* m_entityAliasList{ nullptr };
bool m_dirty{ false };
};
class EntityAliasConstVisitor final : public EntityAliasVisitorBase
{
public:
EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList);
~EntityAliasConstVisitor();
//! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions.
bool IsValid() const;
bool HasAliases() const;
bool AreAllSpawnablesReady() const;
EntityAliasList::const_iterator begin() const;
EntityAliasList::const_iterator end() const;
EntityAliasList::const_iterator cbegin() const;
EntityAliasList::const_iterator cend() const;
void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
private:
const Spawnable& m_owner;
const EntityAliasList* m_entityAliasList;
};
inline static constexpr const char* FileExtension = "spawnable";
inline static constexpr const char* DotFileExtension = ".spawnable";
@@ -45,6 +187,9 @@ namespace AzFramework
const EntityList& GetEntities() const;
EntityList& GetEntities();
EntityAliasConstVisitor TryGetAliasesConst() const;
EntityAliasConstVisitor TryGetAliases() const;
EntityAliasVisitor TryGetAliases();
bool IsEmpty() const;
SpawnableMetaData& GetMetaData();
@@ -55,11 +200,12 @@ namespace AzFramework
private:
SpawnableMetaData m_metaData;
// Aliases that optionally replace the ones stored in this spawnable.
EntityAliasList m_entityAliases;
// Container for keeping all entities of the prefab the Spawnable was created from.
// Includes both direct and nested entities of the prefab.
EntityList m_entities;
mutable AZStd::atomic<int32_t> m_shareState{ ShareState::NotShared };
};
using SpawnableList = AZStd::vector<Spawnable>;
} // namespace AzFramework
@@ -0,0 +1,38 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/std/parallel/mutex.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AzFramework
{
class SpawnableAssetEvents : public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
//! Callback to allow the entity aliases in a spawnable to adjusted based on runtime requirements.
//! This will be called by the Asset Manager as part of the creation of the spawnable asset from loaded file data. Any work done
//! in this callback will be counted towards the maximum amount of time allocated to asset handlers to construct their assets,
//! it's recommended to keep work done in this callback to a minimum and prefer delaying any complex processing.
//!
//! ALERT: Do not start blocking asset requests in this callback.
//! Since this is part of the Asset Manager's asset streaming, doing a blocking load in this callback will cause the job
//! processing the spawnable asset to locked out of doing any asset streaming work. If there are more spawnables doing
//! this than there are job threads available the engine will enter a deadlock situation as no more assets can complete
//! loading and no job threads become free as they're all waiting for assets to complete. It is however safe to queue
//! an asset for loading.
virtual void OnResolveAliases(
Spawnable::EntityAliasVisitor& aliases, const SpawnableMetaData& metadata, const Spawnable::EntityList& entities) = 0;
};
using SpawnableAssetEventsBus = AZ::EBus<SpawnableAssetEvents>;
} // namespace AzFramework
@@ -9,8 +9,10 @@
#include <AzCore/Casting/lossy_cast.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/sort.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
#include <AzFramework/Spawnable/SpawnableAssetBus.h>
namespace AzFramework
{
@@ -52,6 +54,7 @@ namespace AzFramework
AZ::ObjectStream::FilterDescriptor filter(assetLoadFilterCB);
if (AZ::Utils::LoadObjectFromStreamInPlace(*stream, *spawnable, nullptr /*SerializeContext*/, filter))
{
ResolveEntityAliases(spawnable, asset, stream->GetStreamingDeadline(), stream->GetStreamingPriority(), assetLoadFilterCB);
return AZ::Data::AssetHandler::LoadResult::LoadComplete;
}
else
@@ -91,4 +94,41 @@ namespace AzFramework
AZ::Uuid subIdHash = AZ::Uuid::CreateData(id.data(), id.size());
return azlossy_caster(subIdHash.GetHash());
}
void SpawnableAssetHandler::ResolveEntityAliases(
Spawnable* spawnable,
[[maybe_unused]] const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::chrono::milliseconds streamingDeadline,
AZ::IO::IStreamerTypes::Priority streamingPriority,
const AZ::Data::AssetFilterCB& assetLoadFilterCB)
{
Spawnable::EntityAliasVisitor aliases = spawnable->TryGetAliases();
AZ_Assert(aliases.IsValid(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str());
if (aliases.HasAliases())
{
AZ_Assert(
AZStd::is_sorted(
aliases.begin(), aliases.end(),
[](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs)
{
return lhs.HasLowerIndex(rhs);
}),
"Spawnable '%s' has an unsorted entity alias list.", asset.GetHint().c_str());
SpawnableAssetEventsBus::Broadcast(
&SpawnableAssetEvents::OnResolveAliases, aliases, spawnable->GetMetaData(), spawnable->GetEntities());
// The aliases will only be optimized if OnResolveAliases has made any changes.
aliases.Optimize();
aliases.ListSpawnablesRequiringLoad(
[&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset<Spawnable>& assetPendingLoad)
{
AZ::Data::AssetLoadParameters loadInfo;
loadInfo.m_assetLoadFilterCB = assetLoadFilterCB;
loadInfo.m_deadline = streamingDeadline;
loadInfo.m_priority = streamingPriority;
assetPendingLoad.QueueLoad(loadInfo);
});
}
}
} // namespace AzFramework
@@ -50,5 +50,13 @@ namespace AzFramework
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
const AZ::Data::AssetFilterCB& assetLoadFilterCB) override;
private:
void ResolveEntityAliases(
class Spawnable* spawnable,
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::chrono::milliseconds streamingDeadline,
AZ::IO::IStreamerTypes::Priority streamingPriority,
const AZ::Data::AssetFilterCB& assetLoadFilterCB);
};
} // namespace AzFramework
@@ -26,7 +26,7 @@ namespace AzFramework
return m_threadData != nullptr;
}
uint64_t SpawnableEntitiesContainer::GetCurrentGeneration() const
uint32_t SpawnableEntitiesContainer::GetCurrentGeneration() const
{
return m_currentGeneration;
}
@@ -37,7 +37,7 @@ namespace AzFramework
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
}
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<size_t> entityIndices)
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<uint32_t> entityIndices)
{
AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnEntities(
@@ -78,15 +78,21 @@ namespace AzFramework
}
}
void SpawnableEntitiesContainer::Alert(AlertCallback callback)
void SpawnableEntitiesContainer::Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck)
{
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
[generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
});
auto callbackWrapper = [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
};
if (spawnableCheck == CheckIfSpawnableIsLoaded::No)
{
SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket, AZStd::move(callbackWrapper));
}
else
{
SpawnableEntitiesInterface::Get()->LoadBarrier(m_threadData->m_spawnedEntitiesTicket, AZStd::move(callbackWrapper));
}
}
void SpawnableEntitiesContainer::Connect(AZ::Data::Asset<Spawnable> spawnable)
@@ -36,6 +36,12 @@ namespace AzFramework
public:
using AlertCallback = AZStd::function<void(uint32_t generation)>;
enum class CheckIfSpawnableIsLoaded : bool
{
Yes,
No
};
//! Constructs a new spawnables entity container that has not been connected.
SpawnableEntitiesContainer() = default;
//! Constructs a new spawnables entity container that connects to the provided spawnable.
@@ -48,13 +54,13 @@ namespace AzFramework
//! Returns a number that identifies the current generation of the container with. The completion callback can still receive
//! calls from older generations as processing completes on those. The returned value can be used to help calls tell
//! older versions apart from newer ones.
[[nodiscard]] uint64_t GetCurrentGeneration() const;
[[nodiscard]] uint32_t GetCurrentGeneration() const;
//! Puts in a request to spawn entities using all entities in the provided spawnable as a template.
void SpawnAllEntities();
//! Puts in a request to spawn entities using the entities found in the spawnable at the provided indices as a template.
//! @param entityIndices A list of indices to the entities in the spawnable.
void SpawnEntities(AZStd::vector<size_t> entityIndices);
void SpawnEntities(AZStd::vector<uint32_t> entityIndices);
//! Puts in a request to despawn all previous spawned entities.
void DespawnAllEntities();
@@ -73,7 +79,11 @@ namespace AzFramework
//! other than the calling thread including the main thread. Note that because the alert is queued it can still be called
//! after the container has been deleted or can be called for a previously assigned spawnable. In the latter case check
//! if the current generation matches the generation provided with the callback.
void Alert(AlertCallback callback);
//! @param callback The function called when the alert triggers. This can be called from a different thread than the one that
//! the one that made the call to Alert.
//! @param checkSpawnableIsLoaded If true the alert will also block until the spawnable has been loaded. If false then it will
//! be called after all previous calls have completed, but the spawnable may not be loaded at that point.
void Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck = CheckIfSpawnableIsLoaded::No);
private:
void Connect(AZ::Data::Asset<Spawnable> spawnable);
@@ -152,7 +152,7 @@ namespace AzFramework
// SpawnableIndexEntityPair
//
SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator)
SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, uint32_t* indexIterator)
: m_entity(entityIterator)
, m_index(indexIterator)
{
@@ -168,7 +168,7 @@ namespace AzFramework
return *m_entity;
}
size_t SpawnableIndexEntityPair::GetIndex() const
uint32_t SpawnableIndexEntityPair::GetIndex() const
{
return *m_index;
}
@@ -177,7 +177,7 @@ namespace AzFramework
// SpawnableIndexEntityIterator
//
SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator)
SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, uint32_t* indexIterator)
: m_value(entityIterator, indexIterator)
{
}
@@ -248,7 +248,7 @@ namespace AzFramework
//
SpawnableConstIndexEntityContainerView::SpawnableConstIndexEntityContainerView(
AZ::Entity** beginEntity, size_t* beginIndices, size_t length)
AZ::Entity** beginEntity, uint32_t* beginIndices, size_t length)
: m_begin(beginEntity, beginIndices)
, m_end(beginEntity + length, beginIndices + length)
{
@@ -85,19 +85,19 @@ namespace AzFramework
AZ::Entity* GetEntity();
const AZ::Entity* GetEntity() const;
size_t GetIndex() const;
uint32_t GetIndex() const;
private:
SpawnableIndexEntityPair() = default;
SpawnableIndexEntityPair(const SpawnableIndexEntityPair&) = default;
SpawnableIndexEntityPair(SpawnableIndexEntityPair&&) = default;
SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator);
SpawnableIndexEntityPair(AZ::Entity** entityIterator, uint32_t* indexIterator);
SpawnableIndexEntityPair& operator=(const SpawnableIndexEntityPair&) = default;
SpawnableIndexEntityPair& operator=(SpawnableIndexEntityPair&&) = default;
AZ::Entity** m_entity { nullptr };
size_t* m_index { nullptr };
uint32_t* m_index { nullptr };
};
class SpawnableIndexEntityIterator
@@ -110,7 +110,7 @@ namespace AzFramework
using pointer = SpawnableIndexEntityPair*;
using reference = SpawnableIndexEntityPair&;
SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator);
SpawnableIndexEntityIterator(AZ::Entity** entityIterator, uint32_t* indexIterator);
SpawnableIndexEntityIterator& operator++();
SpawnableIndexEntityIterator operator++(int);
@@ -132,7 +132,7 @@ namespace AzFramework
class SpawnableConstIndexEntityContainerView
{
public:
SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, size_t* beginIndices, size_t length);
SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, uint32_t* beginIndices, size_t length);
const SpawnableIndexEntityIterator& begin();
const SpawnableIndexEntityIterator& end();
@@ -144,6 +144,16 @@ namespace AzFramework
SpawnableIndexEntityIterator m_end;
};
//! Information used when updating the type of an entity alias.
struct EntityAliasTypeChange
{
//! The index of the alias in the spawnable. Note that due to optimizations done on the entity aliases the index of an alias
//! can change over time.
uint32_t m_aliasIndex;
//! The type to replace type stored in the spawnable at the index provided by m_aliasIndex.
Spawnable::EntityAliasType m_newAliasType;
};
//! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that is used as a template. A ticket can
//! be reused for multiple calls on the same spawnable and is safe to be used by multiple threads at the same time. Entities created
//! from the spawnable may be tracked by the ticket and so using the same ticket is needed to despawn the exact entities created
@@ -178,6 +188,7 @@ namespace AzFramework
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using RetrieveEntitySpawnTicketCallback = AZStd::function<void(EntitySpawnTicket*)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using UpdateEntityAliasTypesCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
@@ -247,6 +258,15 @@ namespace AzFramework
SpawnablePriority m_priority { SpawnablePriority_Default };
};
struct UpdateEntityAliasTypesOptionalArgs final
{
//! Callback that's called when entity aliases are updated. This can be triggered from a different thread than the one that
//! made the function call to update.
UpdateEntityAliasTypesCallback m_completionCallback;
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
struct ListEntitiesOptionalArgs final
{
//! The priority at which this call will be executed.
@@ -265,6 +285,14 @@ namespace AzFramework
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
struct LoadBarrierOptionalArgs final
{
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
//! Also checks if the spawnables referenced in the entity aliases that are marked to be loaded are loaded.
bool m_checkAliasSpawnables{ true };
};
//! Interface definition to (de)spawn entities from a spawnable into the game world.
//!
//! While the callbacks of the individual calls are being processed they will block processing any other request. Callbacks can be
@@ -298,7 +326,7 @@ namespace AzFramework
//! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from.
//! @param optionalArgs Optional additional arguments, see SpawnEntitiesOptionalArgs.
virtual void SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
//! Removes all entities in the provided list from the environment.
//! @param ticket The ticket previously used to spawn entities with.
//! @param optionalArgs Optional additional arguments, see DespawnAllEntitiesOptionalArgs.
@@ -320,6 +348,16 @@ namespace AzFramework
virtual void ReloadSpawnable(
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) = 0;
//! Allows updating the entity alias on a spawnable. This allows the spawning behavior for all entities spawned from the used
//! spawnable to be changed and is not restricted to this ticket alone.
//! @param ticket Holds the information for the spawnable.
//! @param updateAliases An array of index and alias type values used to update the entity alias list.
//! @param optionalArgs Optional additional arguments, see UpdateEntityAliasTypesOptionalArgs.
virtual void UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs = {}) = 0;
//! List all entities that are spawned using this ticket.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param listCallback Required callback that will be called to list the entities on.
@@ -351,31 +389,37 @@ namespace AzFramework
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
//! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs.
virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) = 0;
//! Blocks until the spawnable is loaded and all operations made on the provided ticket before the barrier call have completed.
//! @param ticket The ticket to monitor.
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
//! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs.
virtual void LoadBarrier(
EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs = {}) = 0;
protected:
[[nodiscard]] virtual AZStd::pair<EntitySpawnTicket::Id, void*> CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
virtual void DestroyTicket(void* ticket) = 0;
template<typename T>
static T& GetTicketPayload(EntitySpawnTicket& ticket)
[[nodiscard]] static T& GetTicketPayload(EntitySpawnTicket& ticket)
{
return *reinterpret_cast<T*>(ticket.m_payload);
}
template<typename T>
static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
[[nodiscard]] static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
{
return *reinterpret_cast<const T*>(ticket.m_payload);
}
template<typename T>
static T* GetTicketPayload(EntitySpawnTicket* ticket)
[[nodiscard]] static T* GetTicketPayload(EntitySpawnTicket* ticket)
{
return reinterpret_cast<T*>(ticket->m_payload);
}
template<typename T>
static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
[[nodiscard]] static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
{
return reinterpret_cast<const T*>(ticket->m_payload);
}
@@ -60,7 +60,7 @@ namespace AzFramework
}
void SpawnableEntitiesManager::SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs)
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized.");
@@ -128,6 +128,20 @@ namespace AzFramework
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized.");
UpdateEntityAliasTypesCommand queueEntry;
queueEntry.m_entityAliases = AZStd::move(updatedAliases);
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback);
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs)
{
@@ -175,6 +189,19 @@ namespace AzFramework
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::LoadBarrier(
EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs)
{
AZ_Assert(completionCallback, "Load barrier on spawnable entities called without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to LoadBarrier hasn't been initialized.");
LoadBarrierCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_checkAliasSpawnables = optionalArgs.m_checkAliasSpawnables;
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
auto SpawnableEntitiesManager::ProcessQueue(CommandQueuePriority priority) -> CommandQueueStatus
{
CommandQueueStatus result = CommandQueueStatus::NoCommandsLeft;
@@ -203,13 +230,13 @@ namespace AzFramework
for (size_t i = 0; i < delayedSize; ++i)
{
Requests& request = queue.m_delayed.front();
bool result = AZStd::visit(
[this](auto&& args) -> bool
CommandResult result = AZStd::visit(
[this](auto&& args) -> CommandResult
{
return ProcessRequest(args);
},
request);
if (!result)
if (result == CommandResult::Requeue)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
@@ -230,13 +257,13 @@ namespace AzFramework
while (!pendingRequestQueue.empty())
{
Requests& request = pendingRequestQueue.front();
bool result = AZStd::visit(
[this](auto&& args) -> bool
CommandResult result = AZStd::visit(
[this](auto&& args) -> CommandResult
{
return ProcessRequest(args);
},
request);
if (!result)
if (result == CommandResult::Requeue)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
@@ -273,14 +300,73 @@ namespace AzFramework
}
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext)
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityPrototype,
EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext)
{
// If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it.
constexpr bool allowDuplicateIds = false;
return AZ::IdUtils::Remapper<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneMap, &serializeContext);
&entityPrototype, prototypeToCloneMap, &serializeContext);
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleAliasedEntity(
const AZ::Entity& entityPrototype,
const Spawnable::EntityAlias& alias,
EntityIdMap& prototypeToCloneMap,
AZ::Entity* previouslySpawnedEntity,
AZ::SerializeContext& serializeContext)
{
AZ::Entity* clone = nullptr;
switch (alias.m_aliasType)
{
case Spawnable::EntityAliasType::Original:
// Behave as the original version.
clone = CloneSingleEntity(entityPrototype, prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Disable:
// Do nothing.
return nullptr;
case Spawnable::EntityAliasType::Replace:
clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Additional:
// The asset handler will have sorted and inserted a Spawnable::EntityAliasType::Original, so the just
// spawn the additional entity.
clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Merge:
AZ_Assert(previouslySpawnedEntity != nullptr, "Merging components but there's no entity to add to yet.");
AppendComponents(
*previouslySpawnedEntity, alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetComponents(), prototypeToCloneMap,
serializeContext);
return nullptr;
default:
AZ_Assert(false, "Unsupported spawnable entity alias type: %i", alias.m_aliasType);
return nullptr;
}
}
void SpawnableEntitiesManager::AppendComponents(
AZ::Entity& target,
const AZ::Entity::ComponentArrayType& componentPrototypes,
EntityIdMap& prototypeToCloneMap,
AZ::SerializeContext& serializeContext)
{
// Only components are added and entities are looked up so no duplicate entity ids should be encountered.
constexpr bool allowDuplicateIds = false;
for (const AZ::Component* component : componentPrototypes)
{
AZ::Component* clone = AZ::IdUtils::Remapper<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
component, prototypeToCloneMap, &serializeContext);
AZ_Assert(clone, "Unable to clone component for entity '%s' (%zu).", target.GetName().c_str(), target.GetId());
[[maybe_unused]] bool result = target.AddComponent(clone);
AZ_Assert(result, "Unable to add cloned component to entity '%s' (%zu).", target.GetName().c_str(), target.GetId());
}
}
void SpawnableEntitiesManager::InitializeEntityIdMappings(
@@ -316,161 +402,264 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
// Keep track how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = entitiesToSpawn.size();
// Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
// Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference
// in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless
// of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to
// previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call.
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst();
aliases.IsValid() && aliases.AreAllSpawnablesReady())
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<uint32_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
// Keep track how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(i);
}
// These are 'prototype' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
uint32_t entitiesToSpawnSize = aznumeric_caster(entitiesToSpawn.size());
// loadAll is true if every entity has been spawned only once
ticket.m_loadAll = (spawnedEntities.size() == entitiesToSpawnSize);
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
(*it)->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
ticket.m_currentRequestId++;
return true;
}
else
{
return false;
}
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ_Assert(
spawnedEntities.size() == spawnedEntityIndices.size(),
"The indices for the spawned entities has gone out of sync with the entities.");
// Keep track of how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities)
{
// This map keeps track of ids from template (spawnable) to clone (instance) allowing patch ups of fields referring
// to entityIds outside of a given entity.
// We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// Pre-generate the full set of entity-id-to-new-entity-id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so
// that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities
// (or SpawnAllEntities) call.
// However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false".
// We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference
// in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless
// of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to
// previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call.
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
}
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
for (size_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
auto aliasIt = aliases.begin();
auto aliasEnd = aliases.end();
if (aliasIt == aliasEnd)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZ::Entity* clone =
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.push_back(clone);
spawnedEntityIndices.push_back(index);
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(i);
}
}
}
ticket.m_loadAll = false;
else
{
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != i)
{
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(i);
}
else
{
// The list of entities has already been sorted and optimized (See SpawnableEntitiesAliasList:Optimize) so can
// be safely executed in order without risking an invalid state.
AZ::Entity* previousEntity = nullptr;
do
{
AZ::Entity* clone = CloneSingleAliasedEntity(
*entitiesToSpawn[i], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity,
*request.m_serializeContext);
previousEntity = clone;
if (clone)
{
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(i);
}
++aliasIt;
} while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == i);
}
}
}
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
(*it)->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
// There were no initial entities then the ticket now holds exactly all entities. If there were already entities then
// a new set are not added so it no longer holds exactly the number of entities.
ticket.m_loadAll = spawnedEntitiesInitialCount == 0;
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
auto newEntitiesBegin = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount;
auto newEntitiesEnd = ticket.m_spawnedEntities.end();
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(newEntitiesBegin, newEntitiesEnd));
}
ticket.m_currentRequestId++;
return true;
}
else
{
return false;
// Add to the game context, now the entities are active
for (auto it = newEntitiesBegin; it != newEntitiesEnd; ++it)
{
AZ::Entity* clone = (*it);
// The entity component framework doesn't handle entities without TransformComponent safely.
if (!clone->GetComponents().empty())
{
clone->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
}
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(newEntitiesBegin, newEntitiesEnd));
}
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
}
return CommandResult::Requeue;
}
bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst();
aliases.IsValid() && aliases.AreAllSpawnablesReady())
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<uint32_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ_Assert(
spawnedEntities.size() == spawnedEntityIndices.size(),
"The indices for the spawned entities has gone out of sync with the entities.");
// Keep track of how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'prototype' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities)
{
// This map keeps track of ids from prototype (spawnable) to clone (instance) allowing patch ups of fields referring
// to entityIds outside of a given entity.
// We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so
// that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities
// (or SpawnAllEntities) call.
// However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false".
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
}
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
auto aliasBegin = aliases.begin();
auto aliasEnd = aliases.end();
if (aliasBegin == aliasEnd)
{
for (uint32_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
spawnedEntities.push_back(
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(index);
}
}
}
else
{
for (uint32_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
auto aliasIt = AZStd::lower_bound(
aliasBegin, aliasEnd, index,
[](const Spawnable::EntityAlias& lhs, uint32_t rhs)
{
return lhs.m_sourceIndex < rhs;
});
if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != index)
{
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(index);
}
else
{
// The list of entities has already been sorted and optimized (See SpawnableEntitiesAliasList:Optimize) so
// can be safely executed in order without risking an invalid state.
AZ::Entity* previousEntity = nullptr;
do
{
AZ::Entity* clone = CloneSingleAliasedEntity(
*entitiesToSpawn[index], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity,
*request.m_serializeContext);
previousEntity = clone;
if (clone)
{
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(index);
}
++aliasIt;
} while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == index);
}
}
}
}
ticket.m_loadAll = false;
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(
request.m_ticketId,
SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
AZ::Entity* clone = (*it);
// The entity component framework doesn't handle entities without TransformComponent safely.
if (!clone->GetComponents().empty())
{
clone->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
}
if (request.m_completionCallback)
{
request.m_completionCallback(
request.m_ticketId,
SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
}
return CommandResult::Requeue;
}
auto SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -495,15 +684,15 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(DespawnEntityCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(DespawnEntityCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -529,15 +718,15 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
AZ_Assert(ticket.m_spawnable.GetId() == request.m_spawnable.GetId(),
@@ -564,7 +753,7 @@ namespace AzFramework
// Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// This map is intentionally cleared out and regenerated here to ensure that we're starting fresh with mappings that
// match the new set of template entities getting spawned.
// match the new set of prototype entities getting spawned.
InitializeEntityIdMappings(entities, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
if (ticket.m_loadAll)
@@ -574,7 +763,7 @@ namespace AzFramework
ticket.m_spawnedEntityIndices.clear();
size_t entitiesToSpawnSize = entities.size();
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entities[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
@@ -590,7 +779,7 @@ namespace AzFramework
{
size_t entitiesSize = entities.size();
for (size_t index : ticket.m_spawnedEntityIndices)
for (uint32_t index : ticket.m_spawnedEntityIndices)
{
// It's possible for the new spawnable to have a different number of entities, so guard against this.
// It's also possible that the entities have moved within the spawnable to a new index. This can't be
@@ -616,15 +805,40 @@ namespace AzFramework
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(UpdateEntityAliasTypesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.IsValid())
{
for (EntityAliasTypeChange& replacement : request.m_entityAliases)
{
aliases.UpdateAliasType(replacement.m_aliasIndex, replacement.m_newAliasType);
}
aliases.Optimize();
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId);
}
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
}
return CommandResult::Requeue;
}
auto SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -632,15 +846,15 @@ namespace AzFramework
request.m_listCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -651,15 +865,15 @@ namespace AzFramework
request.m_listCallback(request.m_ticketId, SpawnableConstIndexEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntityIndices.begin(), ticket.m_spawnedEntities.size()));
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -671,15 +885,15 @@ namespace AzFramework
ticket.m_spawnedEntityIndices.clear();
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -690,15 +904,39 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(LoadBarrierCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
if (request.m_checkAliasSpawnables)
{
if (Spawnable::EntityAliasConstVisitor visitor = ticket.m_spawnable->TryGetAliasesConst();
!visitor.IsValid() || !visitor.AreAllSpawnablesReady())
{
return CommandResult::Requeue;
}
}
request.m_completionCallback(request.m_ticketId);
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
else
{
return CommandResult::Requeue;
}
}
auto SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request) -> CommandResult
{
if (request.m_requestId == request.m_ticket->m_currentRequestId)
{
@@ -706,19 +944,24 @@ namespace AzFramework
{
if (entity != nullptr)
{
// Setting it to 0 is needed to avoid the infite loop between GameEntityContext and SpawnableEntitiesManager.
// Setting it to 0 is needed to avoid the infinite loop between GameEntityContext and SpawnableEntitiesManager.
entity->SetSpawnTicketId(0);
GameEntityContextRequestBus::Broadcast(
&GameEntityContextRequestBus::Events::DestroyGameEntity, entity->GetId());
}
else
{
// Entities without components wouldn't have been send to the GameEntityContext.
delete entity;
}
}
delete request.m_ticket;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
} // namespace AzFramework
@@ -55,13 +55,18 @@ namespace AzFramework
void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
void DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void DespawnEntity(AZ::EntityId entityId, EntitySpawnTicket& ticket, DespawnEntityOptionalArgs optionalArgs = {}) override;
void RetrieveEntitySpawnTicket(EntitySpawnTicket::Id entitySpawnTicketId, RetrieveEntitySpawnTicketCallback callback) override;
void ReloadSpawnable(
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) override;
void UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs = {}) override;
void ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs = {}) override;
void ListIndicesAndEntities(
@@ -70,6 +75,8 @@ namespace AzFramework
EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs = {}) override;
void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) override;
void LoadBarrier(
EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs = {}) override;
//
// The following function is thread safe but intended to be run from the main thread.
@@ -78,14 +85,20 @@ namespace AzFramework
CommandQueueStatus ProcessQueue(CommandQueuePriority priority);
protected:
struct Ticket
enum class CommandResult : bool
{
Executed,
Requeue
};
struct Ticket final
{
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
static constexpr uint32_t Processing = AZStd::numeric_limits<uint32_t>::max();
//! Map of template entity ids to their associated instance ids.
//! Tickets can be used to spawn the same template entities multiple times, in any order, across multiple calls.
//! Since template entities can reference other entities, this map is used to fix up those references across calls
//! Map of prototype entity ids to their associated instance ids.
//! Tickets can be used to spawn the same prototype entities multiple times, in any order, across multiple calls.
//! Since prototype entities can reference other entities, this map is used to fix up those references across calls
//! using the following policy:
//! - Entities referencing an entity that hasn't been spawned yet will get a reference to the id that *will* be used
//! the first time that entity will be spawned. The reference will be invalid until that entity is spawned, but
@@ -100,14 +113,14 @@ namespace AzFramework
AZStd::unordered_set<AZ::EntityId> m_previouslySpawned;
AZStd::vector<AZ::Entity*> m_spawnedEntities;
AZStd::vector<size_t> m_spawnedEntityIndices;
AZStd::vector<uint32_t> m_spawnedEntityIndices;
AZ::Data::Asset<Spawnable> m_spawnable;
uint32_t m_nextRequestId{ 0 }; //!< Next id for this ticket.
uint32_t m_currentRequestId { 0 }; //!< The id for the command that should be executed.
bool m_loadAll{ true };
};
struct SpawnAllEntitiesCommand
struct SpawnAllEntitiesCommand final
{
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
@@ -116,9 +129,9 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct SpawnEntitiesCommand
struct SpawnEntitiesCommand final
{
AZStd::vector<size_t> m_entityIndices;
AZStd::vector<uint32_t> m_entityIndices;
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
AZ::SerializeContext* m_serializeContext;
@@ -127,7 +140,7 @@ namespace AzFramework
uint32_t m_requestId;
bool m_referencePreviouslySpawnedEntities;
};
struct DespawnAllEntitiesCommand
struct DespawnAllEntitiesCommand final
{
EntityDespawnCallback m_completionCallback;
Ticket* m_ticket;
@@ -142,7 +155,7 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ReloadSpawnableCommand
struct ReloadSpawnableCommand final
{
AZ::Data::Asset<Spawnable> m_spawnable;
ReloadSpawnableCallback m_completionCallback;
@@ -151,35 +164,51 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListEntitiesCommand
struct UpdateEntityAliasTypesCommand final
{
AZStd::vector<EntityAliasTypeChange> m_entityAliases;
UpdateEntityAliasTypesCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListEntitiesCommand final
{
ListEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListIndicesEntitiesCommand
struct ListIndicesEntitiesCommand final
{
ListIndicesEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ClaimEntitiesCommand
struct ClaimEntitiesCommand final
{
ClaimEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct BarrierCommand
struct BarrierCommand final
{
BarrierCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct DestroyTicketCommand
struct LoadBarrierCommand final
{
BarrierCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
bool m_checkAliasSpawnables;
};
struct DestroyTicketCommand final
{
Ticket* m_ticket;
uint32_t m_requestId;
@@ -191,10 +220,12 @@ namespace AzFramework
DespawnAllEntitiesCommand,
DespawnEntityCommand,
ReloadSpawnableCommand,
UpdateEntityAliasTypesCommand,
ListEntitiesCommand,
ListIndicesEntitiesCommand,
ClaimEntitiesCommand,
BarrierCommand,
LoadBarrierCommand,
DestroyTicketCommand>;
struct Queue
@@ -212,18 +243,31 @@ namespace AzFramework
CommandQueueStatus ProcessQueue(Queue& queue);
AZ::Entity* CloneSingleEntity(
const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext);
const AZ::Entity& entityPrototype, EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext);
AZ::Entity* CloneSingleAliasedEntity(
const AZ::Entity& entityPrototype,
const Spawnable::EntityAlias& alias,
EntityIdMap& prototypeToCloneMap,
AZ::Entity* previouslySpawnedEntity,
AZ::SerializeContext& serializeContext);
void AppendComponents(
AZ::Entity& target,
const AZ::Entity::ComponentArrayType& componentPrototypes,
EntityIdMap& prototypeToCloneMap,
AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnAllEntitiesCommand& request);
bool ProcessRequest(SpawnEntitiesCommand& request);
bool ProcessRequest(DespawnAllEntitiesCommand& request);
bool ProcessRequest(DespawnEntityCommand& request);
bool ProcessRequest(ReloadSpawnableCommand& request);
bool ProcessRequest(ListEntitiesCommand& request);
bool ProcessRequest(ListIndicesEntitiesCommand& request);
bool ProcessRequest(ClaimEntitiesCommand& request);
bool ProcessRequest(BarrierCommand& request);
bool ProcessRequest(DestroyTicketCommand& request);
CommandResult ProcessRequest(SpawnAllEntitiesCommand& request);
CommandResult ProcessRequest(SpawnEntitiesCommand& request);
CommandResult ProcessRequest(DespawnAllEntitiesCommand& request);
CommandResult ProcessRequest(DespawnEntityCommand& request);
CommandResult ProcessRequest(ReloadSpawnableCommand& request);
CommandResult ProcessRequest(UpdateEntityAliasTypesCommand& request);
CommandResult ProcessRequest(ListEntitiesCommand& request);
CommandResult ProcessRequest(ListIndicesEntitiesCommand& request);
CommandResult ProcessRequest(ClaimEntitiesCommand& request);
CommandResult ProcessRequest(BarrierCommand& request);
CommandResult ProcessRequest(LoadBarrierCommand& request);
CommandResult ProcessRequest(DestroyTicketCommand& request);
//! Generate a base set of original-to-new entity ID mappings to use during spawning.
//! Since Entity references get fixed up on an entity-by-entity basis while spawning, it's important to have the complete
@@ -72,7 +72,7 @@ namespace AzFramework
uint64_t SpawnableSystemComponent::AssignRootSpawnable(AZ::Data::Asset<Spawnable> rootSpawnable)
{
uint64_t generation = 0;
uint32_t generation = 0;
if (m_rootSpawnableId == rootSpawnable.GetId())
{
@@ -87,16 +87,25 @@ namespace AzFramework
// Suspend and resume processing in the container that completion calls aren't received until
// everything has been setup to accept callbacks from the call.
m_rootSpawnableContainer.Reset(rootSpawnable);
m_rootSpawnableContainer.SpawnAllEntities();
generation = m_rootSpawnableContainer.GetCurrentGeneration();
AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(),
generation);
// Don't send out the alert that the root spawnable has been assigned until the spawnable itself is ready. The common
// use case is for handlers to do something with the information in the spawnable before the entities get spawned.
m_rootSpawnableContainer.Alert(
[rootSpawnable](uint32_t generation)
{
RootSpawnableNotificationBus::Broadcast(
&RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, AZStd::move(rootSpawnable), generation);
}, SpawnableEntitiesContainer::CheckIfSpawnableIsLoaded::Yes);
m_rootSpawnableContainer.SpawnAllEntities();
m_rootSpawnableContainer.Alert(
[newSpawnable = AZStd::move(rootSpawnable)](uint32_t generation)
{
RootSpawnableNotificationBus::QueueBroadcast(
&RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, newSpawnable, generation);
&RootSpawnableNotificationBus::Events::OnRootSpawnableReady, AZStd::move(newSpawnable), generation);
});
AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(), generation);
}
else
{
@@ -132,6 +141,12 @@ namespace AzFramework
AZ_TracePrintf("Spawnables", "New root spawnable '%s' assigned (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation);
}
void SpawnableSystemComponent::OnRootSpawnableReady(
[[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable, [[maybe_unused]] uint32_t generation)
{
AZ_TracePrintf("Spawnables", "Entities from new root spawnable '%s' are ready (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation);
}
void SpawnableSystemComponent::OnRootSpawnableReleased([[maybe_unused]] uint32_t generation)
{
AZ_TracePrintf("Spawnables", "Generation %i of the root spawnable has been released.\n", generation);
@@ -82,6 +82,7 @@ namespace AzFramework
//
void OnRootSpawnableAssigned(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReady(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReleased(uint32_t generation) override;
protected:
@@ -806,12 +806,20 @@ namespace AzFramework
[[maybe_unused]] float scrollDelta,
[[maybe_unused]] float deltaTime)
{
const auto pivot = m_pivotFn();
if (!pivot.has_value())
{
EndActivation();
return targetCamera;
}
if (Beginning())
{
// as the camera starts, record the camera we would like to end up as
m_nextCamera.m_offset = m_offsetFn(m_pivotFn().GetDistance(targetCamera.Translation()));
m_nextCamera.m_offset = m_offsetFn(pivot.value().GetDistance(targetCamera.Translation()));
const auto angles =
EulerAngles(AZ::Matrix3x3::CreateFromMatrix3x4(AZ::Matrix3x4::CreateLookAt(targetCamera.Translation(), m_pivotFn())));
EulerAngles(AZ::Matrix3x3::CreateFromMatrix3x4(AZ::Matrix3x4::CreateLookAt(targetCamera.Translation(), pivot.value())));
m_nextCamera.m_pitch = angles.GetX();
m_nextCamera.m_yaw = angles.GetZ();
m_nextCamera.m_pivot = targetCamera.m_pivot;
@@ -651,7 +651,7 @@ namespace AzFramework
class FocusCameraInput : public CameraInput
{
public:
using PivotFn = AZStd::function<AZ::Vector3()>;
using PivotFn = AZStd::function<AZStd::optional<AZ::Vector3>()>;
FocusCameraInput(const InputChannelId& focusChannelId, FocusOffsetFn offsetFn);
@@ -8,8 +8,9 @@
#pragma once
#include <AzFramework/Viewport/ViewportId.h>
#include <AzCore/EBus/EBus.h>
#include <AzCore/std/optional.h>
#include <AzFramework/Viewport/ViewportId.h>
namespace AZ
{
@@ -20,18 +21,15 @@ namespace AZ
namespace AzFramework
{
class ViewportRequests
: public AZ::EBusTraits
class ViewportRequests : public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = ViewportId;
static void Reflect(AZ::ReflectContext* context);
virtual ~ViewportRequests() {}
//! Gets the current camera's world to view matrix.
virtual const AZ::Matrix4x4& GetCameraViewMatrix() const = 0;
//! Sets the current camera's world to view matrix.
@@ -44,8 +42,36 @@ namespace AzFramework
virtual AZ::Transform GetCameraTransform() const = 0;
//! Convenience method, sets the camera's world to view matrix from this AZ::Transform.
virtual void SetCameraTransform(const AZ::Transform& transform) = 0;
protected:
~ViewportRequests() = default;
};
using ViewportRequestBus = AZ::EBus<ViewportRequests>;
} //namespace AzFramework
//! The additional padding around the viewport when a viewport border is active.
struct ViewportBorderPadding
{
float m_top;
float m_bottom;
float m_left;
float m_right;
};
//! For performing queries about the state of the viewport border.
class ViewportBorderRequests : public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = ViewportId;
//! Returns if a viewport border is in effect and what the current dimensions (padding) of the border are.
virtual AZStd::optional<ViewportBorderPadding> GetViewportBorderPadding() const = 0;
protected:
~ViewportBorderRequests() = default;
};
using ViewportBorderRequestBus = AZ::EBus<ViewportBorderRequests>;
} // namespace AzFramework
@@ -286,6 +286,7 @@ set(FILES
Spawnable/RootSpawnableInterface.h
Spawnable/Spawnable.cpp
Spawnable/Spawnable.h
Spawnable/SpawnableAssetBus.h
Spawnable/SpawnableAssetHandler.h
Spawnable/SpawnableAssetHandler.cpp
Spawnable/SpawnableEntitiesContainer.h
@@ -10,6 +10,8 @@
#include <AzFramework/XcbEventHandler.h>
#include <AzFramework/XcbInterface.h>
#include <xcb/xinput.h>
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
@@ -34,6 +36,31 @@ namespace AzFramework
return m_xcbConnection.get();
}
void SetEnableXInput(xcb_connection_t* connection, bool enable) override
{
struct Mask
{
xcb_input_event_mask_t head;
xcb_input_xi_event_mask_t mask;
};
const Mask mask {
/*.head=*/{
/*.device_id=*/XCB_INPUT_DEVICE_ALL_MASTER,
/*.mask_len=*/1
},
/*.mask=*/ enable ?
(xcb_input_xi_event_mask_t)(XCB_INPUT_XI_EVENT_MASK_RAW_MOTION | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_PRESS | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_RELEASE) :
(xcb_input_xi_event_mask_t)XCB_NONE
};
const xcb_setup_t* xcbSetup = xcb_get_setup(connection);
const xcb_screen_t* xcbScreen = xcb_setup_roots_iterator(xcbSetup).data;
xcb_input_xi_select_events(connection, xcbScreen->root, 1, &mask.head);
xcb_flush(connection);
}
private:
XcbUniquePtr<xcb_connection_t, xcb_disconnect> m_xcbConnection = nullptr;
};
@@ -24,6 +24,9 @@ namespace AzFramework
virtual ~XcbConnectionManager() = default;
virtual xcb_connection_t* GetXcbConnection() const = 0;
//! Enables/Disables XInput Raw Input events.
virtual void SetEnableXInput(xcb_connection_t* connection, bool enable) = 0;
};
class XcbConnectionManagerBusTraits
@@ -23,9 +23,6 @@ namespace AzFramework
virtual ~XcbEventHandler() = default;
virtual void HandleXcbEvent(xcb_generic_event_t* event) = 0;
// ATTN This is used as a workaround for RAW Input events when using the Editor.
virtual void PollSpecialEvents(){};
};
class XcbEventHandlerBusTraits : public AZ::EBusTraits
@@ -13,21 +13,68 @@
namespace AzFramework
{
xcb_window_t GetSystemCursorFocusWindow()
xcb_window_t GetSystemCursorFocusWindow(xcb_connection_t* connection)
{
void* systemCursorFocusWindow = nullptr;
AzFramework::InputSystemCursorConstraintRequestBus::BroadcastResult(
systemCursorFocusWindow, &AzFramework::InputSystemCursorConstraintRequests::GetSystemCursorConstraintWindow);
if (!systemCursorFocusWindow)
if (systemCursorFocusWindow)
{
return XCB_NONE;
return static_cast<xcb_window_t>(reinterpret_cast<uint64_t>(systemCursorFocusWindow));
}
// TODO Clang compile error because cast .... loses information. On GNU/Linux HWND is void* and on 64-bit
// machines its obviously 64 bit but we receive the window id from m_renderOverlay.winId() which is xcb_window_t 32-bit.
// EWMH-compliant window managers set the "_NET_ACTIVE_WINDOW" property
// of the X server's root window to the currently active window. This
// retrieves value of that property.
return static_cast<xcb_window_t>(reinterpret_cast<uint64_t>(systemCursorFocusWindow));
// Get the atom for the _NET_ACTIVE_WINDOW property
constexpr int propertyNameLength = 18;
xcb_generic_error_t* error = nullptr;
XcbStdFreePtr<xcb_intern_atom_reply_t> activeWindowAtom {xcb_intern_atom_reply(
connection,
xcb_intern_atom(connection, /*only_if_exists=*/ 1, propertyNameLength, "_NET_ACTIVE_WINDOW"),
&error
)};
if (!activeWindowAtom || error)
{
if (error)
{
AZ_Warning("XcbInput", false, "Retrieving _NET_ACTIVE_WINDOW atom failed : Error code %d", error->error_code);
free(error);
}
return XCB_WINDOW_NONE;
}
// Get the root window
const xcb_window_t rootWId = xcb_setup_roots_iterator(xcb_get_setup(connection)).data->root;
// Fetch the value of the root window's _NET_ACTIVE_WINDOW property
XcbStdFreePtr<xcb_get_property_reply_t> property {xcb_get_property_reply(
connection,
xcb_get_property(
/*c=*/connection,
/*_delete=*/ 0,
/*window=*/rootWId,
/*property=*/activeWindowAtom->atom,
/*type=*/XCB_ATOM_WINDOW,
/*long_offset=*/0,
/*long_length=*/1
),
&error
)};
if (!property || error)
{
if (error)
{
AZ_Warning("XcbInput", false, "Retrieving _NET_ACTIVE_WINDOW atom failed : Error code %d", error->error_code);
free(error);
}
return XCB_WINDOW_NONE;
}
return *static_cast<xcb_window_t*>(xcb_get_property_value(property.get()));
}
xcb_connection_t* XcbInputDeviceMouse::s_xcbConnection = nullptr;
@@ -39,8 +86,7 @@ namespace AzFramework
: InputDeviceMouse::Implementation(inputDevice)
, m_systemCursorState(SystemCursorState::Unknown)
, m_systemCursorPositionNormalized(0.5f, 0.5f)
, m_prevConstraintWindow(XCB_NONE)
, m_focusWindow(XCB_NONE)
, m_focusWindow(XCB_WINDOW_NONE)
, m_cursorShown(true)
{
XcbEventHandlerBus::Handler::BusConnect();
@@ -57,14 +103,14 @@ namespace AzFramework
InputDeviceMouse::Implementation* XcbInputDeviceMouse::Create(InputDeviceMouse& inputDevice)
{
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
const auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
if (!interface)
{
AZ_Warning("XcbInput", false, "XCB interface not available");
return nullptr;
}
s_xcbConnection = AzFramework::XcbConnectionManagerInterface::Get()->GetXcbConnection();
s_xcbConnection = interface->GetXcbConnection();
if (!s_xcbConnection)
{
AZ_Warning("XcbInput", false, "XCB connection not available");
@@ -126,7 +172,7 @@ namespace AzFramework
// Get window information.
const XcbStdFreePtr<xcb_get_geometry_reply_t> xcbGeometryReply{ xcb_get_geometry_reply(
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) };
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) };
if (!xcbGeometryReply)
{
@@ -137,7 +183,7 @@ namespace AzFramework
xcb_translate_coordinates(s_xcbConnection, window, s_xcbScreen->root, 0, 0);
const XcbStdFreePtr<xcb_translate_coordinates_reply_t> xkbTranslateCoordReply{ xcb_translate_coordinates_reply(
s_xcbConnection, translate_coord, NULL) };
s_xcbConnection, translate_coord, nullptr) };
if (!xkbTranslateCoordReply)
{
@@ -173,11 +219,11 @@ namespace AzFramework
for (const auto& barrier : m_activeBarriers)
{
xcb_void_cookie_t cookie = xcb_xfixes_create_pointer_barrier_checked(
s_xcbConnection, barrier.id, window, barrier.x0, barrier.y0, barrier.x1, barrier.y1, barrier.direction, 0, NULL);
const XcbStdFreePtr<xcb_generic_error_t> xkbError{ xcb_request_check(s_xcbConnection, cookie) };
s_xcbConnection, barrier.id, window, barrier.x0, barrier.y0, barrier.x1, barrier.y1, barrier.direction, 0, nullptr);
const XcbStdFreePtr<xcb_generic_error_t> xcbError{ xcb_request_check(s_xcbConnection, cookie) };
AZ_Warning(
"XcbInput", !xkbError, "XFixes, failed to create barrier %d at (%d %d %d %d)", barrier.id, barrier.x0, barrier.y0,
"XcbInput", !xcbError, "XFixes, failed to create barrier %d at (%d %d %d %d)", barrier.id, barrier.x0, barrier.y0,
barrier.x1, barrier.y1);
}
}
@@ -207,7 +253,7 @@ namespace AzFramework
const xcb_xfixes_query_version_cookie_t query_cookie = xcb_xfixes_query_version(s_xcbConnection, 5, 0);
xcb_generic_error_t* error = NULL;
xcb_generic_error_t* error = nullptr;
const XcbStdFreePtr<xcb_xfixes_query_version_reply_t> xkbQueryRequestReply{ xcb_xfixes_query_version_reply(
s_xcbConnection, query_cookie, &error) };
@@ -244,7 +290,7 @@ namespace AzFramework
const xcb_input_xi_query_version_cookie_t query_version_cookie = xcb_input_xi_query_version(s_xcbConnection, 2, 2);
xcb_generic_error_t* error = NULL;
xcb_generic_error_t* error = nullptr;
const XcbStdFreePtr<xcb_input_xi_query_version_reply_t> xkbQueryRequestReply{ xcb_input_xi_query_version_reply(
s_xcbConnection, query_version_cookie, &error) };
@@ -268,40 +314,13 @@ namespace AzFramework
return m_xInputInitialized;
}
void XcbInputDeviceMouse::SetEnableXInput(bool enable)
{
struct
{
xcb_input_event_mask_t head;
int mask;
} mask;
mask.head.deviceid = XCB_INPUT_DEVICE_ALL;
mask.head.mask_len = 1;
if (enable)
{
mask.mask = XCB_INPUT_XI_EVENT_MASK_RAW_MOTION | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_PRESS |
XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_RELEASE | XCB_INPUT_XI_EVENT_MASK_MOTION | XCB_INPUT_XI_EVENT_MASK_BUTTON_PRESS |
XCB_INPUT_XI_EVENT_MASK_BUTTON_RELEASE;
}
else
{
mask.mask = XCB_NONE;
}
xcb_input_xi_select_events(s_xcbConnection, s_xcbScreen->root, 1, &mask.head);
xcb_flush(s_xcbConnection);
}
void XcbInputDeviceMouse::SetSystemCursorState(SystemCursorState systemCursorState)
{
if (systemCursorState != m_systemCursorState)
{
m_systemCursorState = systemCursorState;
m_focusWindow = GetSystemCursorFocusWindow();
m_focusWindow = GetSystemCursorFocusWindow(s_xcbConnection);
HandleCursorState(m_focusWindow, systemCursorState);
}
@@ -309,52 +328,10 @@ namespace AzFramework
void XcbInputDeviceMouse::HandleCursorState(xcb_window_t window, SystemCursorState systemCursorState)
{
bool confined = false, cursorShown = true;
switch (systemCursorState)
{
case SystemCursorState::ConstrainedAndHidden:
{
//!< Constrained to the application's main window and hidden
confined = true;
cursorShown = false;
}
break;
case SystemCursorState::ConstrainedAndVisible:
{
//!< Constrained to the application's main window and visible
confined = true;
}
break;
case SystemCursorState::UnconstrainedAndHidden:
{
//!< Free to move outside the main window but hidden while inside
cursorShown = false;
}
break;
case SystemCursorState::UnconstrainedAndVisible:
{
//!< Free to move outside the application's main window and visible
}
case SystemCursorState::Unknown:
default:
break;
}
// ATTN GetSystemCursorFocusWindow when getting out of the play in editor will return XCB_NONE
// We need however the window id to reset the cursor.
if (XCB_NONE == window && (confined || cursorShown))
{
// Reuse the previous window to reset states.
window = m_prevConstraintWindow;
m_prevConstraintWindow = XCB_NONE;
}
else
{
// Remember the window we used to modify cursor and barrier states.
m_prevConstraintWindow = window;
}
SetEnableXInput(!cursorShown);
const bool confined = (systemCursorState == SystemCursorState::ConstrainedAndHidden) ||
(systemCursorState == SystemCursorState::ConstrainedAndVisible);
const bool cursorShown = (systemCursorState == SystemCursorState::ConstrainedAndVisible) ||
(systemCursorState == SystemCursorState::UnconstrainedAndVisible);
CreateBarriers(window, confined);
ShowCursor(window, cursorShown);
@@ -368,26 +345,26 @@ namespace AzFramework
void XcbInputDeviceMouse::SetSystemCursorPositionNormalizedInternal(xcb_window_t window, AZ::Vector2 positionNormalized)
{
// TODO Basically not done at all. Added only the basic functions needed.
const XcbStdFreePtr<xcb_get_geometry_reply_t> xkbGeometryReply{ xcb_get_geometry_reply(
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) };
const XcbStdFreePtr<xcb_get_geometry_reply_t> xcbGeometryReply{ xcb_get_geometry_reply(
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) };
if (!xkbGeometryReply)
if (!xcbGeometryReply)
{
return;
}
const int16_t x = static_cast<int16_t>(positionNormalized.GetX() * xkbGeometryReply->width);
const int16_t y = static_cast<int16_t>(positionNormalized.GetY() * xkbGeometryReply->height);
const int16_t x = static_cast<int16_t>(positionNormalized.GetX() * xcbGeometryReply->width);
const int16_t y = static_cast<int16_t>(positionNormalized.GetY() * xcbGeometryReply->height);
xcb_warp_pointer(s_xcbConnection, XCB_NONE, window, 0, 0, 0, 0, x, y);
xcb_warp_pointer(s_xcbConnection, XCB_WINDOW_NONE, window, 0, 0, 0, 0, x, y);
xcb_flush(s_xcbConnection);
}
void XcbInputDeviceMouse::SetSystemCursorPositionNormalized(AZ::Vector2 positionNormalized)
{
const xcb_window_t window = GetSystemCursorFocusWindow();
if (XCB_NONE == window)
const xcb_window_t window = GetSystemCursorFocusWindow(s_xcbConnection);
if (XCB_WINDOW_NONE == window)
{
return;
}
@@ -401,7 +378,7 @@ namespace AzFramework
const xcb_query_pointer_cookie_t pointer = xcb_query_pointer(s_xcbConnection, window);
const XcbStdFreePtr<xcb_query_pointer_reply_t> xkbQueryPointerReply{ xcb_query_pointer_reply(s_xcbConnection, pointer, NULL) };
const XcbStdFreePtr<xcb_query_pointer_reply_t> xkbQueryPointerReply{ xcb_query_pointer_reply(s_xcbConnection, pointer, nullptr) };
if (!xkbQueryPointerReply)
{
@@ -409,7 +386,7 @@ namespace AzFramework
}
const XcbStdFreePtr<xcb_get_geometry_reply_t> xkbGeometryReply{ xcb_get_geometry_reply(
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) };
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) };
if (!xkbGeometryReply)
{
@@ -429,8 +406,8 @@ namespace AzFramework
AZ::Vector2 XcbInputDeviceMouse::GetSystemCursorPositionNormalized() const
{
const xcb_window_t window = GetSystemCursorFocusWindow();
if (XCB_NONE == window)
const xcb_window_t window = GetSystemCursorFocusWindow(s_xcbConnection);
if (XCB_WINDOW_NONE == window)
{
return AZ::Vector2::CreateZero();
}
@@ -455,11 +432,11 @@ namespace AzFramework
cookie = xcb_xfixes_hide_cursor_checked(s_xcbConnection, window);
}
const XcbStdFreePtr<xcb_generic_error_t> xkbError{ xcb_request_check(s_xcbConnection, cookie) };
const XcbStdFreePtr<xcb_generic_error_t> xcbError{ xcb_request_check(s_xcbConnection, cookie) };
if (xkbError)
if (xcbError)
{
AZ_Warning("XcbInput", false, "ShowCursor failed: %d", xkbError->error_code);
AZ_Warning("XcbInput", false, "ShowCursor failed: %d", xcbError->error_code);
return;
}
@@ -500,14 +477,6 @@ namespace AzFramework
}
}
void XcbInputDeviceMouse::HandlePointerMotionEvents(const xcb_generic_event_t* event)
{
const xcb_input_motion_event_t* mouseMotionEvent = reinterpret_cast<const xcb_input_motion_event_t*>(event);
m_systemCursorPosition[0] = mouseMotionEvent->event_x;
m_systemCursorPosition[1] = mouseMotionEvent->event_y;
}
void XcbInputDeviceMouse::HandleRawInputEvents(const xcb_ge_generic_event_t* event)
{
const xcb_ge_generic_event_t* genericEvent = reinterpret_cast<const xcb_ge_generic_event_t*>(event);
@@ -552,78 +521,20 @@ namespace AzFramework
}
}
void XcbInputDeviceMouse::PollSpecialEvents()
{
while (xcb_generic_event_t* genericEvent = xcb_poll_for_queued_event(s_xcbConnection))
{
// TODO Is the following correct? If we are showing the cursor, don't poll RAW Input events.
switch (genericEvent->response_type & ~0x80)
{
case XCB_GE_GENERIC:
{
const xcb_ge_generic_event_t* geGenericEvent = reinterpret_cast<const xcb_ge_generic_event_t*>(genericEvent);
// Only handle raw inputs if we have focus.
// Handle Raw Input events first.
if ((geGenericEvent->event_type == XCB_INPUT_RAW_BUTTON_PRESS) ||
(geGenericEvent->event_type == XCB_INPUT_RAW_BUTTON_RELEASE) ||
(geGenericEvent->event_type == XCB_INPUT_RAW_MOTION))
{
HandleRawInputEvents(geGenericEvent);
free(genericEvent);
}
}
break;
}
}
}
void XcbInputDeviceMouse::HandleXcbEvent(xcb_generic_event_t* event)
{
switch (event->response_type & ~0x80)
{
// QT5 is using by default XInput which means we do need to check for XCB_GE_GENERIC event to parse all mouse related events.
// XInput raw events are sent from the server as a XCB_GE_GENERIC
// event. A XCB_GE_GENERIC event is typecast to a
// xcb_ge_generic_event_t, which is distinct from a
// xcb_generic_event_t, and exists so that X11 extensions can extend
// the event emission beyond the size that a normal X11 event could
// contain.
case XCB_GE_GENERIC:
{
const xcb_ge_generic_event_t* genericEvent = reinterpret_cast<const xcb_ge_generic_event_t*>(event);
// Handling RAW Inputs here works in GameMode but not in Editor mode because QT is
// not handling RAW input events and passing to.
if (!m_cursorShown)
{
// Handle Raw Input events first.
if ((genericEvent->event_type == XCB_INPUT_RAW_BUTTON_PRESS) ||
(genericEvent->event_type == XCB_INPUT_RAW_BUTTON_RELEASE) || (genericEvent->event_type == XCB_INPUT_RAW_MOTION))
{
HandleRawInputEvents(genericEvent);
}
}
else
{
switch (genericEvent->event_type)
{
case XCB_INPUT_BUTTON_PRESS:
{
const xcb_input_button_press_event_t* mouseButtonEvent =
reinterpret_cast<const xcb_input_button_press_event_t*>(genericEvent);
HandleButtonPressEvents(mouseButtonEvent->detail, true);
}
break;
case XCB_INPUT_BUTTON_RELEASE:
{
const xcb_input_button_release_event_t* mouseButtonEvent =
reinterpret_cast<const xcb_input_button_release_event_t*>(genericEvent);
HandleButtonPressEvents(mouseButtonEvent->detail, false);
}
break;
case XCB_INPUT_MOTION:
{
HandlePointerMotionEvents(event);
}
break;
}
}
HandleRawInputEvents(genericEvent);
}
break;
case XCB_FOCUS_IN:
@@ -634,6 +545,9 @@ namespace AzFramework
m_focusWindow = focusInEvent->event;
HandleCursorState(m_focusWindow, m_systemCursorState);
}
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
interface->SetEnableXInput(interface->GetXcbConnection(), true);
}
break;
case XCB_FOCUS_OUT:
@@ -644,7 +558,10 @@ namespace AzFramework
ProcessRawEventQueues();
ResetInputChannelStates();
m_focusWindow = XCB_NONE;
m_focusWindow = XCB_WINDOW_NONE;
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
interface->SetEnableXInput(interface->GetXcbConnection(), false);
}
break;
}
@@ -65,9 +65,6 @@ namespace AzFramework
//! \ref AzFramework::InputDeviceMouse::Implementation::TickInputDevice
void TickInputDevice() override;
//! This method is called by the Editor to accommodate some events with the Editor. Never called in Game mode.
void PollSpecialEvents() override;
//! Handle X11 events.
void HandleXcbEvent(xcb_generic_event_t* event) override;
@@ -77,9 +74,6 @@ namespace AzFramework
//! Initialize XInput extension. Used for raw input during confinement and showing/hiding the cursor.
static bool InitializeXInput();
//! Enables/Disables XInput Raw Input events.
void SetEnableXInput(bool enable);
//! Create barriers.
void CreateBarriers(xcb_window_t window, bool create);
@@ -98,9 +92,6 @@ namespace AzFramework
//! Handle button press/release events.
void HandleButtonPressEvents(uint32_t detail, bool pressed);
//! Handle motion notify events.
void HandlePointerMotionEvents(const xcb_generic_event_t* event);
//! Will set cursor states and confinement modes.
void HandleCursorState(xcb_window_t window, SystemCursorState systemCursorState);
@@ -160,7 +151,6 @@ namespace AzFramework
AZ::Vector2 m_cursorHiddenPosition;
AZ::Vector2 m_systemCursorPositionNormalized;
uint32_t m_systemCursorPosition[MAX_XI_RAW_AXIS];
static xcb_connection_t* s_xcbConnection;
static xcb_screen_t* s_xcbScreen;
@@ -171,9 +161,6 @@ namespace AzFramework
//! Will be true if the xinput2 extension could be initialized.
static bool m_xInputInitialized;
//! The window that had focus
xcb_window_t m_prevConstraintWindow;
//! The current window that has focus
xcb_window_t m_focusWindow;
@@ -9,19 +9,71 @@
#include <AzCore/IO/Path/Path.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/std/containers/array.h>
#include <AzCore/std/tuple.h>
#include <errno.h>
#include <cerrno>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <unistd.h>
AZ_CVAR(bool, ap_tether_lifetime, true, nullptr, AZ::ConsoleFunctorFlags::Null,
"If enabled, a parent process that launches the AP will terminate the AP on exit");
namespace AzFramework::AssetSystem::Platform
{
void AllowAssetProcessorToForeground()
{}
[[noreturn]] static void LaunchAssetProcessorDirectly(const AZ::IO::FixedMaxPath& assetProcessorPath, AZStd::string_view engineRoot, AZStd::string_view projectPath)
{
AZStd::fixed_vector<const char*, 5> args {
assetProcessorPath.c_str(),
"--start-hidden",
};
// Add the engine path to the launch command if not empty
AZ::IO::FixedMaxPathString engineRootArg;
if (!engineRoot.empty())
{
// No need to quote these paths, this code calls exec directly and
// does not go through shell string interpolation
engineRootArg = AZ::IO::FixedMaxPathString{"--engine-path="} + AZ::IO::FixedMaxPathString{engineRoot};
args.push_back(engineRootArg.data());
}
// Add the active project path to the launch command if not empty
AZ::IO::FixedMaxPathString projectPathArg;
if (!projectPath.empty())
{
projectPathArg = AZ::IO::FixedMaxPathString{"--regset=/Amazon/AzCore/Bootstrap/project_path="} + AZ::IO::FixedMaxPathString{projectPath};
args.push_back(projectPathArg.data());
}
// Make sure this is at the end
args.push_back(nullptr); // argv itself needs to be null-terminated
execv(args[0], const_cast<char**>(args.data()));
// exec* family of functions only return on error
fprintf(stderr, "Asset Processor failed with error: %s\n", strerror(errno));
_exit(1);
}
static pid_t LaunchAssetProcessorDaemonized(const AZ::IO::FixedMaxPath& assetProcessorPath, AZStd::string_view engineRoot, AZStd::string_view projectPath)
{
// detach the child from parent
setsid();
const pid_t secondChildPid = fork();
if (secondChildPid == 0)
{
LaunchAssetProcessorDirectly(assetProcessorPath, engineRoot, projectPath);
}
return secondChildPid;
}
bool LaunchAssetProcessor(AZStd::string_view executableDirectory, AZStd::string_view engineRoot,
AZStd::string_view projectPath)
{
@@ -40,7 +92,8 @@ namespace AzFramework::AssetSystem::Platform
}
}
pid_t firstChildPid = fork();
const pid_t parentPid = getpid();
const pid_t firstChildPid = fork();
if (firstChildPid == 0)
{
// redirect output to dev/null so it doesn't hijack an existing console window
@@ -53,51 +106,33 @@ namespace AzFramework::AssetSystem::Platform
AZ::IO::FileDescriptorRedirector stderrRedirect(STDERR_FILENO);
stderrRedirect.RedirectTo(devNull, mode);
// detach the child from parent
setsid();
pid_t secondChildPid = fork();
if (secondChildPid == 0)
if (ap_tether_lifetime)
{
AZStd::array args {
assetProcessorPath.c_str(), assetProcessorPath.c_str(), "--start-hidden",
static_cast<const char*>(nullptr), static_cast<const char*>(nullptr), static_cast<const char*>(nullptr)
};
int optionalArgPos = 3;
// Add the engine path to the launch command if not empty
AZ::IO::FixedMaxPathString engineRootArg;
if (!engineRoot.empty())
prctl(PR_SET_PDEATHSIG, SIGTERM);
if (getppid() != parentPid)
{
engineRootArg = AZ::IO::FixedMaxPathString::format(R"(--engine-path="%.*s")",
aznumeric_cast<int>(engineRoot.size()), engineRoot.data());
args[optionalArgPos++] = engineRootArg.data();
_exit(1);
}
LaunchAssetProcessorDirectly(assetProcessorPath, engineRoot, projectPath);
}
else
{
const pid_t secondChildPid = LaunchAssetProcessorDaemonized(assetProcessorPath, engineRoot, projectPath);
stdoutRedirect.Reset();
stderrRedirect.Reset();
// Add the active project path to the launch command if not empty
AZ::IO::FixedMaxPathString projectPathArg;
if (!projectPath.empty())
{
projectPathArg = AZ::IO::FixedMaxPathString::format(R"(--regset="/Amazon/AzCore/Bootstrap/project_path=%.*s")",
aznumeric_cast<int>(projectPath.size()), projectPath.data());
args[optionalArgPos++] = projectPathArg.data();
}
AZStd::apply(execl, args);
// exec* family of functions only exit on error
AZ_Error("AssetSystemComponent", false, "Asset Processor failed with error: %s", strerror(errno));
_exit(1);
// exit the transient child with proper return code
int ret = (secondChildPid < 0) ? 1 : 0;
_exit(ret);
}
stdoutRedirect.Reset();
stderrRedirect.Reset();
// exit the transient child with proper return code
int ret = (secondChildPid < 0) ? 1 : 0;
_exit(ret);
}
else if (firstChildPid > 0)
{
if (ap_tether_lifetime)
{
return true;
}
// wait for first child to exit to ensure the second child was started
int status = 0;
pid_t ret = waitpid(firstChildPid, &status, 0);
@@ -106,4 +141,4 @@ namespace AzFramework::AssetSystem::Platform
return false;
}
}
} // namespace AzFramework::AssetSystem::Platform
@@ -11,6 +11,7 @@
#include <AzCore/Module/DynamicModuleHandle.h>
#include <AzCore/PlatformIncl.h>
#include <AzCore/std/containers/array.h>
#include <AzCore/std/string/conversions.h>
namespace AzFramework
@@ -234,14 +235,14 @@ namespace AzFramework
const UINT rawInputHeaderSize = sizeof(RAWINPUTHEADER);
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &rawInputSize, rawInputHeaderSize);
LPBYTE rawInputBytes = new BYTE[rawInputSize];
AZStd::array<BYTE, sizeof(RAWINPUT)> rawInputBytesArray;
LPBYTE rawInputBytes = rawInputBytesArray.data();
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, rawInputBytes, &rawInputSize, rawInputHeaderSize);
RAWINPUT* rawInput = (RAWINPUT*)rawInputBytes;
AzFramework::RawInputNotificationBusWindows::Broadcast(
&AzFramework::RawInputNotificationBusWindows::Events::OnRawInputEvent, *rawInput);
delete [] rawInputBytes;
break;
}
case WM_CHAR:
@@ -41,7 +41,9 @@ namespace UnitTest
auto projectPathKey =
AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
registry->Set(projectPathKey, "AutomatedTesting");
AZ::IO::FixedMaxPath enginePath;
registry->Get(enginePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
registry->Set(projectPathKey, (enginePath / "AutomatedTesting").Native());
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
m_application->Start({});
@@ -45,7 +45,9 @@ namespace UnitTest
auto projectPathKey =
AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
registry->Set(projectPathKey, "AutomatedTesting");
AZ::IO::FixedMaxPath enginePath;
registry->Get(enginePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
registry->Set(projectPathKey, (enginePath / "AutomatedTesting").Native());
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
m_application->Start({});
@@ -305,7 +305,9 @@ namespace UnitTest
AZ::SettingsRegistryInterface* registry = AZ::SettingsRegistry::Get();
auto projectPathKey =
AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
registry->Set(projectPathKey, "AutomatedTesting");
AZ::IO::FixedMaxPath enginePath;
registry->Get(enginePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
registry->Set(projectPathKey, (enginePath / "AutomatedTesting").Native());
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
AZ::ComponentApplication::StartupParameters startupParameters;
@@ -48,6 +48,24 @@ namespace InputUnitTests
AZStd::unique_ptr<InputSystemComponent> m_inputSystemComponent;
};
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputChannelId_ConstExpression_CopyConstructorSuccessfull)
{
constexpr InputChannelId testInputChannelId1("TestInputChannelId");
constexpr InputChannelId testInputChannelId2(testInputChannelId1);
static_assert(testInputChannelId1 == testInputChannelId2);
EXPECT_EQ(testInputChannelId1, testInputChannelId2);
}
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputDeviceId_ConstExpression_CopyConstructorSuccessfull)
{
constexpr InputDeviceId testInputDeviceId1("TestInputDeviceId");
constexpr InputDeviceId testInputDeviceId2(testInputDeviceId1);
static_assert(testInputDeviceId1 == testInputDeviceId2);
EXPECT_EQ(testInputDeviceId1, testInputDeviceId2);
}
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputContext_InitWithDataStruct_InitializationSuccessfull)
{
@@ -36,7 +36,7 @@ namespace AzFramework
MOCK_METHOD3(
SpawnEntities,
void(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs));
void(EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs));
MOCK_METHOD2(DespawnAllEntities, void(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs));
@@ -49,6 +49,13 @@ namespace AzFramework
ReloadSpawnable,
void(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs));
MOCK_METHOD3(
UpdateEntityAliasTypes,
void(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs));
MOCK_METHOD3(
ListEntities, void(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs));
@@ -61,6 +68,7 @@ namespace AzFramework
void(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs));
MOCK_METHOD3(Barrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs));
MOCK_METHOD3(LoadBarrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs));
MOCK_METHOD1(CreateTicket, AZStd::pair<EntitySpawnTicket::Id, void*>(AZ::Data::Asset<Spawnable>&& spawnable));
MOCK_METHOD1(DestroyTicket, void(void* ticket));
@@ -11,6 +11,13 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_0_VALUE_PARAMS()) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{};
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_1_VALUE_PARAMS(p0)) {
@@ -25,3 +32,38 @@ ACTION_TEMPLATE(ReturnMalloc,
*value = T{ p0, p1 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_3_VALUE_PARAMS(p0, p1, p2)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_4_VALUE_PARAMS(p0, p1, p2, p3)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2, p3 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_5_VALUE_PARAMS(p0, p1, p2, p3, p4)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2, p3, p4 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_6_VALUE_PARAMS(p0, p1, p2, p3, p4, p5)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2, p3, p4, p5 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_7_VALUE_PARAMS(p0, p1, p2, p3, p4, p5, p6)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2, p3, p4, p5, p6 };
return value;
}
@@ -32,6 +32,82 @@ xcb_generic_error_t* xcb_request_check(xcb_connection_t* c, xcb_void_cookie_t co
{
return MockXcbInterface::Instance()->xcb_request_check(c, cookie);
}
const xcb_setup_t* xcb_get_setup(xcb_connection_t *c)
{
return MockXcbInterface::Instance()->xcb_get_setup(c);
}
xcb_screen_iterator_t xcb_setup_roots_iterator(const xcb_setup_t* R)
{
return MockXcbInterface::Instance()->xcb_setup_roots_iterator(R);
}
const xcb_query_extension_reply_t* xcb_get_extension_data(xcb_connection_t* c, xcb_extension_t* ext)
{
return MockXcbInterface::Instance()->xcb_get_extension_data(c, ext);
}
int xcb_flush(xcb_connection_t *c)
{
return MockXcbInterface::Instance()->xcb_flush(c);
}
xcb_query_pointer_cookie_t xcb_query_pointer(xcb_connection_t* c, xcb_window_t window)
{
return MockXcbInterface::Instance()->xcb_query_pointer(c, window);
}
xcb_query_pointer_reply_t* xcb_query_pointer_reply(xcb_connection_t* c, xcb_query_pointer_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_query_pointer_reply(c, cookie, e);
}
xcb_get_geometry_cookie_t xcb_get_geometry(xcb_connection_t* c, xcb_drawable_t drawable)
{
return MockXcbInterface::Instance()->xcb_get_geometry(c, drawable);
}
xcb_get_geometry_reply_t* xcb_get_geometry_reply(xcb_connection_t* c, xcb_get_geometry_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_get_geometry_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_warp_pointer(
xcb_connection_t* c,
xcb_window_t src_window,
xcb_window_t dst_window,
int16_t src_x,
int16_t src_y,
uint16_t src_width,
uint16_t src_height,
int16_t dst_x,
int16_t dst_y)
{
return MockXcbInterface::Instance()->xcb_warp_pointer(c, src_window, dst_window, src_x, src_y, src_width, src_height, dst_x, dst_y);
}
xcb_intern_atom_cookie_t xcb_intern_atom(xcb_connection_t* c, uint8_t only_if_exists, uint16_t name_len, const char* name)
{
return MockXcbInterface::Instance()->xcb_intern_atom(c, only_if_exists, name_len, name);
}
xcb_intern_atom_reply_t* xcb_intern_atom_reply(xcb_connection_t* c, xcb_intern_atom_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_intern_atom_reply(c, cookie, e);
}
xcb_get_property_cookie_t xcb_get_property(
xcb_connection_t* c,
uint8_t _delete,
xcb_window_t window,
xcb_atom_t property,
xcb_atom_t type,
uint32_t long_offset,
uint32_t long_length)
{
return MockXcbInterface::Instance()->xcb_get_property(c, _delete, window, property, type, long_offset, long_length);
}
xcb_get_property_reply_t* xcb_get_property_reply(xcb_connection_t* c, xcb_get_property_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_get_property_reply(c, cookie, e);
}
void* xcb_get_property_value(const xcb_get_property_reply_t* R)
{
return MockXcbInterface::Instance()->xcb_get_property_value(R);
}
uint32_t xcb_generate_id(xcb_connection_t *c)
{
return MockXcbInterface::Instance()->xcb_generate_id(c);
}
// ----------------------------------------------------------------------------
// xcb-xkb
@@ -116,4 +192,76 @@ xkb_state_component xkb_state_update_mask(
state, depressed_mods, latched_mods, locked_mods, depressed_layout, latched_layout, locked_layout);
}
// ----------------------------------------------------------------------------
// xcb-xfixes
xcb_xfixes_query_version_cookie_t xcb_xfixes_query_version(
xcb_connection_t* c, uint32_t client_major_version, uint32_t client_minor_version)
{
return MockXcbInterface::Instance()->xcb_xfixes_query_version(c, client_major_version, client_minor_version);
}
xcb_xfixes_query_version_reply_t* xcb_xfixes_query_version_reply(
xcb_connection_t* c, xcb_xfixes_query_version_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_xfixes_query_version_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_xfixes_show_cursor_checked(xcb_connection_t* c, xcb_window_t window)
{
return MockXcbInterface::Instance()->xcb_xfixes_show_cursor_checked(c, window);
}
xcb_void_cookie_t xcb_xfixes_hide_cursor_checked(xcb_connection_t* c, xcb_window_t window)
{
return MockXcbInterface::Instance()->xcb_xfixes_hide_cursor_checked(c, window);
}
xcb_void_cookie_t xcb_xfixes_delete_pointer_barrier_checked(xcb_connection_t* c, xcb_xfixes_barrier_t barrier)
{
return MockXcbInterface::Instance()->xcb_xfixes_delete_pointer_barrier_checked(c, barrier);
}
xcb_translate_coordinates_cookie_t xcb_translate_coordinates(xcb_connection_t* c, xcb_window_t src_window, xcb_window_t dst_window, int16_t src_x, int16_t src_y)
{
return MockXcbInterface::Instance()->xcb_translate_coordinates(c, src_window, dst_window, src_x, src_y);
}
xcb_translate_coordinates_reply_t* xcb_translate_coordinates_reply(xcb_connection_t* c, xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_translate_coordinates_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_xfixes_create_pointer_barrier_checked(
xcb_connection_t* c,
xcb_xfixes_barrier_t barrier,
xcb_window_t window,
uint16_t x1,
uint16_t y1,
uint16_t x2,
uint16_t y2,
uint32_t directions,
uint16_t num_devices,
const uint16_t* devices)
{
return MockXcbInterface::Instance()->xcb_xfixes_create_pointer_barrier_checked(c, barrier, window, x1, y1, x2, y2, directions, num_devices, devices);
}
// ----------------------------------------------------------------------------
// xcb-xinput
xcb_input_xi_query_version_cookie_t xcb_input_xi_query_version(xcb_connection_t* c, uint16_t major_version, uint16_t minor_version)
{
return MockXcbInterface::Instance()->xcb_input_xi_query_version(c, major_version, minor_version);
}
xcb_input_xi_query_version_reply_t* xcb_input_xi_query_version_reply(
xcb_connection_t* c, xcb_input_xi_query_version_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_input_xi_query_version_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_input_xi_select_events(
xcb_connection_t* c, xcb_window_t window, uint16_t num_mask, const xcb_input_event_mask_t* masks)
{
return MockXcbInterface::Instance()->xcb_input_xi_select_events(c, window, num_mask, masks);
}
int xcb_input_raw_button_press_axisvalues_length (const xcb_input_raw_button_press_event_t *R)
{
return MockXcbInterface::Instance()->xcb_input_raw_button_press_axisvalues_length(R);
}
xcb_input_fp3232_t* xcb_input_raw_button_press_axisvalues_raw(const xcb_input_raw_button_press_event_t* R)
{
return MockXcbInterface::Instance()->xcb_input_raw_button_press_axisvalues_raw(R);
}
}
@@ -18,6 +18,8 @@
#undef explicit
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-x11.h>
#include <xcb/xfixes.h>
#include <xcb/xinput.h>
#include "Printers.h"
@@ -62,6 +64,37 @@ public:
MOCK_CONST_METHOD1(xcb_disconnect, void(xcb_connection_t* c));
MOCK_CONST_METHOD1(xcb_poll_for_event, xcb_generic_event_t*(xcb_connection_t* c));
MOCK_CONST_METHOD2(xcb_request_check, xcb_generic_error_t*(xcb_connection_t* c, xcb_void_cookie_t cookie));
MOCK_CONST_METHOD1(xcb_get_setup, const xcb_setup_t*(xcb_connection_t *c));
MOCK_CONST_METHOD1(xcb_setup_roots_iterator, xcb_screen_iterator_t(const xcb_setup_t* R));
MOCK_CONST_METHOD2(xcb_get_extension_data, const xcb_query_extension_reply_t*(xcb_connection_t* c, xcb_extension_t* ext));
MOCK_CONST_METHOD1(xcb_flush, int(xcb_connection_t *c));
MOCK_CONST_METHOD2(xcb_query_pointer, xcb_query_pointer_cookie_t(xcb_connection_t* c, xcb_window_t window));
MOCK_CONST_METHOD3(xcb_query_pointer_reply, xcb_query_pointer_reply_t*(xcb_connection_t* c, xcb_query_pointer_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD2(xcb_get_geometry, xcb_get_geometry_cookie_t(xcb_connection_t* c, xcb_drawable_t drawable));
MOCK_CONST_METHOD3(xcb_get_geometry_reply, xcb_get_geometry_reply_t*(xcb_connection_t* c, xcb_get_geometry_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD9(xcb_warp_pointer, xcb_void_cookie_t(
xcb_connection_t* c,
xcb_window_t src_window,
xcb_window_t dst_window,
int16_t src_x,
int16_t src_y,
uint16_t src_width,
uint16_t src_height,
int16_t dst_x,
int16_t dst_y));
MOCK_CONST_METHOD4(xcb_intern_atom, xcb_intern_atom_cookie_t(xcb_connection_t* c, uint8_t only_if_exists, uint16_t name_len, const char* name));
MOCK_CONST_METHOD3(xcb_intern_atom_reply, xcb_intern_atom_reply_t*(xcb_connection_t* c, xcb_intern_atom_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD7(xcb_get_property, xcb_get_property_cookie_t(
xcb_connection_t* c,
uint8_t _delete,
xcb_window_t window,
xcb_atom_t property,
xcb_atom_t type,
uint32_t long_offset,
uint32_t long_length));
MOCK_CONST_METHOD3(xcb_get_property_reply, xcb_get_property_reply_t*(xcb_connection_t* c, xcb_get_property_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD1(xcb_get_property_value, void*(const xcb_get_property_reply_t* R));
MOCK_CONST_METHOD1(xcb_generate_id, uint32_t(xcb_connection_t *c));
// xcb-xkb
MOCK_CONST_METHOD3(xcb_xkb_use_extension, xcb_xkb_use_extension_cookie_t(xcb_connection_t* c, uint16_t wantedMajor, uint16_t wantedMinor));
@@ -83,6 +116,33 @@ public:
MOCK_CONST_METHOD4(xkb_state_key_get_utf8, int(xkb_state* state, xkb_keycode_t key, char* buffer, size_t size));
MOCK_CONST_METHOD7(xkb_state_update_mask, xkb_state_component(xkb_state* state, xkb_mod_mask_t depressed_mods, xkb_mod_mask_t latched_mods, xkb_mod_mask_t locked_mods, xkb_layout_index_t depressed_layout, xkb_layout_index_t latched_layout, xkb_layout_index_t locked_layout));
// xcb-xfixes
MOCK_CONST_METHOD3(xcb_xfixes_query_version, xcb_xfixes_query_version_cookie_t(xcb_connection_t* c, uint32_t client_major_version, uint32_t client_minor_version));
MOCK_CONST_METHOD3(xcb_xfixes_query_version_reply, xcb_xfixes_query_version_reply_t*(xcb_connection_t* c, xcb_xfixes_query_version_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD2(xcb_xfixes_show_cursor_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window));
MOCK_CONST_METHOD2(xcb_xfixes_hide_cursor_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window));
MOCK_CONST_METHOD2(xcb_xfixes_delete_pointer_barrier_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_xfixes_barrier_t barrier));
MOCK_CONST_METHOD5(xcb_translate_coordinates, xcb_translate_coordinates_cookie_t(xcb_connection_t* c, xcb_window_t src_window, xcb_window_t dst_window, int16_t src_x, int16_t src_y));
MOCK_CONST_METHOD3(xcb_translate_coordinates_reply, xcb_translate_coordinates_reply_t*(xcb_connection_t* c, xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD10(xcb_xfixes_create_pointer_barrier_checked, xcb_void_cookie_t(
xcb_connection_t* c,
xcb_xfixes_barrier_t barrier,
xcb_window_t window,
uint16_t x1,
uint16_t y1,
uint16_t x2,
uint16_t y2,
uint32_t directions,
uint16_t num_devices,
const uint16_t* devices));
// xcb-xinput
MOCK_CONST_METHOD3(xcb_input_xi_query_version, xcb_input_xi_query_version_cookie_t(xcb_connection_t* c, uint16_t major_version, uint16_t minor_version));
MOCK_CONST_METHOD3(xcb_input_xi_query_version_reply, xcb_input_xi_query_version_reply_t*(xcb_connection_t* c, xcb_input_xi_query_version_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD4(xcb_input_xi_select_events, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window, uint16_t num_mask, const xcb_input_event_mask_t* masks));
MOCK_CONST_METHOD1(xcb_input_raw_button_press_axisvalues_length, int(const xcb_input_raw_button_press_event_t* R));
MOCK_CONST_METHOD1(xcb_input_raw_button_press_axisvalues_raw, xcb_input_fp3232_t*(const xcb_input_raw_button_press_event_t* R));
private:
static inline MockXcbInterface* self = nullptr;
};
@@ -22,6 +22,12 @@ namespace AzFramework
public:
void SetUp() override;
template<typename T>
static xcb_generic_event_t MakeEvent(T event)
{
return *reinterpret_cast<xcb_generic_event_t*>(&event);
}
protected:
testing::NiceMock<MockXcbInterface> m_interface;
xcb_connection_t m_connection{};
@@ -21,12 +21,6 @@
#include "XcbBaseTestFixture.h"
#include "XcbTestApplication.h"
template<typename T>
xcb_generic_event_t MakeEvent(T event)
{
return *reinterpret_cast<xcb_generic_event_t*>(&event);
}
namespace AzFramework
{
// Sets up default behavior for mock keyboard responses to xcb methods
@@ -0,0 +1,545 @@
/*
* 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 <gtest/gtest.h>
#include <gmock/gmock.h>
#include <xcb/xcb.h>
#include <AzFramework/Input/Buses/Requests/InputSystemCursorRequestBus.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include "XcbBaseTestFixture.h"
#include "XcbTestApplication.h"
#include "Matchers.h"
#include "Actions.h"
namespace AzFramework
{
// Sets up default behavior for mock keyboard responses to xcb methods
class XcbInputDeviceMouseTests
: public XcbBaseTestFixture
{
public:
void SetUp() override
{
using testing::Eq;
using testing::Field;
using testing::Return;
using testing::StrEq;
using testing::_;
XcbBaseTestFixture::SetUp();
ON_CALL(m_interface, xcb_get_setup(&m_connection))
.WillByDefault(Return(&s_xcbSetup));
ON_CALL(m_interface, xcb_setup_roots_iterator(&s_xcbSetup))
.WillByDefault(Return(xcb_screen_iterator_t{&s_xcbScreen}));
ON_CALL(m_interface, xcb_get_extension_data(&m_connection, &xcb_xfixes_id))
.WillByDefault(Return(&s_xfixesExtensionReply));
ON_CALL(m_interface, xcb_xfixes_query_version_reply(&m_connection, _, _))
.WillByDefault(ReturnMalloc<xcb_xfixes_query_version_reply_t>(
/*response_type=*/(uint8_t)XCB_XFIXES_QUERY_VERSION,
/*pad0=*/(uint8_t)0,
/*sequence=*/(uint16_t)1,
/*length=*/0u,
/*major_version=*/5u,
/*minor_version=*/0u
));
ON_CALL(m_interface, xcb_get_extension_data(&m_connection, &xcb_input_id))
.WillByDefault(Return(&s_xfixesExtensionReply));
ON_CALL(m_interface, xcb_input_xi_query_version_reply(&m_connection, _, _))
.WillByDefault(ReturnMalloc<xcb_input_xi_query_version_reply_t>(
/*response_type=*/(uint8_t)XCB_INPUT_XI_QUERY_VERSION,
/*pad0=*/(uint8_t)0,
/*sequence=*/(uint16_t)1,
/*length=*/0u,
/*major_version=*/(uint16_t)2,
/*minor_version=*/(uint16_t)2
));
// Set the default focus window
EXPECT_CALL(m_interface, xcb_intern_atom(&m_connection, 1, 18, StrEq("_NET_ACTIVE_WINDOW")))
.WillRepeatedly(Return(xcb_intern_atom_cookie_t{/*.sequence=*/ 1}));
ON_CALL(m_interface, xcb_intern_atom_reply(&m_connection, Field(&xcb_intern_atom_cookie_t::sequence, Eq(1)), _))
.WillByDefault(ReturnMalloc<xcb_intern_atom_reply_t>(
/*response_type=*/(uint8_t)XCB_INTERN_ATOM,
/*pad0=*/(uint8_t)0,
/*sequence=*/(uint16_t)1,
/*length=*/0u,
/*xcb_atom_t=*/s_netActiveWindowAtom
));
ON_CALL(m_interface, xcb_get_property(&m_connection, 0, s_rootWindow, s_netActiveWindowAtom, XCB_ATOM_WINDOW, 0, 1))
.WillByDefault(Return(xcb_get_property_cookie_t{/*.sequence=*/ s_getActiveWindowPropertySequence}));
ON_CALL(m_interface, xcb_get_property_reply(&m_connection, Field(&xcb_get_property_cookie_t::sequence, Eq(s_getActiveWindowPropertySequence)), _))
.WillByDefault(ReturnMalloc<xcb_get_property_reply_t>(
/*response_type=*/(uint8_t)XCB_GET_PROPERTY,
/*format=*/(uint8_t)0,
/*sequence=*/(uint16_t)s_getActiveWindowPropertySequence,
/*length=*/0u,
/*type=*/XCB_ATOM_WINDOW,
/*bytes_after=*/0u,
/*value_len=*/1u
));
ON_CALL(m_interface, xcb_get_property_value(Field(&xcb_get_property_reply_t::sequence, Eq(s_getActiveWindowPropertySequence))))
.WillByDefault(Return(const_cast<xcb_window_t*>(&s_nullWindow)));
ON_CALL(m_interface, xcb_get_geometry(&m_connection, _))
.WillByDefault(Return(xcb_get_geometry_cookie_t{/*.sequence=*/1}));
ON_CALL(m_interface, xcb_get_geometry_reply(&m_connection, Field(&xcb_get_geometry_cookie_t::sequence, Eq(1)), _))
.WillByDefault(ReturnMalloc<xcb_get_geometry_reply_t>(s_defaultWindowGeometry));
}
void PumpApplication()
{
m_application.PumpSystemEventLoopUntilEmpty();
m_application.TickSystem();
m_application.Tick();
}
protected:
static constexpr inline uint8_t s_xinputMajorOpcode = 131;
static constexpr inline xcb_window_t s_rootWindow = 1;
static constexpr inline xcb_window_t s_nullWindow = XCB_WINDOW_NONE;
static constexpr inline xcb_input_device_id_t s_virtualCorePointerId = 2;
static constexpr inline xcb_input_device_id_t s_physicalPointerDeviceId = 3;
static constexpr inline uint16_t s_screenWidthInPixels = 3840;
static constexpr inline uint16_t s_screenHeightInPixels = 2160;
static constexpr inline uint16_t s_getActiveWindowPropertySequence = 2160;
static constexpr inline xcb_atom_t s_netActiveWindowAtom = 1;
static constexpr inline xcb_setup_t s_xcbSetup{
/*.status=*/1,
/*.pad0=*/0,
/*.protocol_major_version=*/11,
/*.protocol_minor_version=*/0,
};
static inline xcb_screen_t s_xcbScreen{
/*.root=*/s_rootWindow,
/*.default_colormap=*/32,
/*.white_pixel=*/16777215,
/*.black_pixel=*/0,
/*.current_input_masks=*/0,
/*.width_in_pixels=*/s_screenWidthInPixels,
/*.height_in_pixels=*/s_screenHeightInPixels,
/*.width_in_millimeters=*/602,
/*.height_in_millimeters=*/341,
};
static constexpr inline xcb_query_extension_reply_t s_xfixesExtensionReply{
/*.response_type=*/XCB_QUERY_EXTENSION,
/*.pad0=*/0,
/*.sequence=*/1,
/*.length=*/0,
/*.present=*/1,
};
static constexpr inline xcb_query_extension_reply_t s_xinputExtensionReply{
/*.response_type=*/XCB_QUERY_EXTENSION,
/*.pad0=*/0,
/*.sequence=*/1,
/*.length=*/0,
/*.present=*/1,
/*.major_opcode=*/s_xinputMajorOpcode,
};
static constexpr inline xcb_get_geometry_reply_t s_defaultWindowGeometry{
/*.response_type=*/XCB_GET_GEOMETRY,
/*.depth=*/0,
/*.sequence=*/1,
/*.length=*/0,
/*.root=*/s_rootWindow,
/*.x=*/100,
/*.y=*/100,
/*.width=*/100,
/*.height=*/100,
/*.border_width=*/3,
/*.pad0[2]=*/{},
};
XcbTestApplication m_application{
/*enabledGamepadsCount=*/0,
/*keyboardEnabled=*/false,
/*motionEnabled=*/false,
/*mouseEnabled=*/true,
/*touchEnabled=*/false,
/*virtualKeyboardEnabled=*/false
};
};
struct MouseButtonTestData
{
xcb_button_index_t m_button;
};
class XcbInputDeviceMouseButtonTests
: public XcbInputDeviceMouseTests
, public testing::WithParamInterface<MouseButtonTestData>
{
public:
static InputChannelId GetInputChannelIdForButton(const xcb_button_index_t button)
{
switch (button)
{
case XCB_BUTTON_INDEX_1:
return InputDeviceMouse::Button::Left;
case XCB_BUTTON_INDEX_2:
return InputDeviceMouse::Button::Right;
case XCB_BUTTON_INDEX_3:
return InputDeviceMouse::Button::Middle;
}
return InputChannelId{};
}
AZStd::array<InputChannelId, 4> GetIdleChannelIdsForButton(const xcb_button_index_t button)
{
switch (button)
{
case XCB_BUTTON_INDEX_1:
return { InputDeviceMouse::Button::Right, InputDeviceMouse::Button::Middle, InputDeviceMouse::Button::Other1, InputDeviceMouse::Button::Other2 };
case XCB_BUTTON_INDEX_2:
return { InputDeviceMouse::Button::Left, InputDeviceMouse::Button::Middle, InputDeviceMouse::Button::Other1, InputDeviceMouse::Button::Other2 };
case XCB_BUTTON_INDEX_3:
return { InputDeviceMouse::Button::Left, InputDeviceMouse::Button::Right, InputDeviceMouse::Button::Other1, InputDeviceMouse::Button::Other2 };
case XCB_BUTTON_INDEX_4:
return { InputDeviceMouse::Button::Left, InputDeviceMouse::Button::Right, InputDeviceMouse::Button::Middle, InputDeviceMouse::Button::Other2 };
case XCB_BUTTON_INDEX_5:
return { InputDeviceMouse::Button::Left, InputDeviceMouse::Button::Right, InputDeviceMouse::Button::Middle, InputDeviceMouse::Button::Other1 };
}
return AZStd::array<InputChannelId, 4>();
}
};
TEST_P(XcbInputDeviceMouseButtonTests, ButtonInputChannelsUpdateStateFromXcbEvents)
{
using testing::Each;
using testing::Eq;
using testing::NotNull;
using testing::Property;
using testing::Return;
// Set the expectations for the events that will be generated
// nullptr entries represent when the event queue is empty, and will cause
// PumpSystemEventLoopUntilEmpty to return
//
// Event pointers are freed by the calling code, so these actions
// malloc new copies
//
// The xcb mouse does not react to the `XCB_BUTTON_PRESS` /
// `XCB_BUTTON_RELEASE` events, but it will still receive those events
// from the X server.
EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(MakeEvent(xcb_input_raw_button_press_event_t{
/*response_type=*/XCB_GE_GENERIC,
/*extension=*/s_xinputMajorOpcode,
/*sequence=*/4,
/*length=*/2,
/*event_type=*/XCB_INPUT_RAW_BUTTON_PRESS,
/*deviceid=*/s_virtualCorePointerId,
/*time=*/3984920,
/*detail=*/GetParam().m_button,
/*sourceid=*/s_physicalPointerDeviceId,
/*valuators_len=*/2,
/*flags=*/0,
/*pad0[4]=*/{},
/*full_sequence=*/4
})))
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(MakeEvent(xcb_button_press_event_t{
/*response_type=*/XCB_BUTTON_PRESS,
/*detail=*/static_cast<xcb_button_t>(GetParam().m_button),
/*sequence=*/4,
/*time=*/3984920,
/*root=*/s_rootWindow,
/*event=*/119537664,
/*child=*/0,
/*root_x=*/55,
/*root_y=*/1099,
/*event_x=*/55,
/*event_y=*/55,
/*state=*/0,
/*same_screen=*/1
})))
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(MakeEvent(xcb_input_raw_button_release_event_t{
/*response_type=*/XCB_GE_GENERIC,
/*extension=*/s_xinputMajorOpcode,
/*sequence=*/4,
/*length=*/2,
/*event_type=*/XCB_INPUT_RAW_BUTTON_RELEASE,
/*deviceid=*/s_virtualCorePointerId,
/*time=*/3984964,
/*detail=*/GetParam().m_button,
/*sourceid=*/s_physicalPointerDeviceId,
/*valuators_len=*/2,
/*flags=*/0,
/*pad0[4]=*/{},
/*full_sequence=*/4
})))
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(MakeEvent(xcb_button_release_event_t{
/*response_type=*/XCB_BUTTON_RELEASE,
/*detail=*/static_cast<xcb_button_t>(GetParam().m_button),
/*sequence=*/4,
/*time=*/3984964,
/*root=*/s_rootWindow,
/*event=*/119537664,
/*child=*/0,
/*root_x=*/55,
/*root_y=*/1099,
/*event_x=*/55,
/*event_y=*/55,
/*state=*/XCB_KEY_BUT_MASK_BUTTON_1,
/*same_screen=*/1
})))
.WillOnce(Return(nullptr))
;
m_application.Start();
InputSystemCursorRequestBus::Event(
InputDeviceMouse::Id,
&InputSystemCursorRequests::SetSystemCursorState,
SystemCursorState::ConstrainedAndHidden);
const InputChannel* activeButtonChannel = InputChannelRequests::FindInputChannel(GetInputChannelIdForButton(GetParam().m_button));
const auto inactiveButtonChannels = [this]()
{
const auto inactiveButtonChannelIds = GetIdleChannelIdsForButton(GetParam().m_button);
AZStd::array<const InputChannel*, 4> channels{};
AZStd::transform(begin(inactiveButtonChannelIds), end(inactiveButtonChannelIds), begin(channels), [](const InputChannelId& id)
{
return InputChannelRequests::FindInputChannel(id);
});
return channels;
}();
ASSERT_TRUE(activeButtonChannel);
ASSERT_THAT(inactiveButtonChannels, Each(NotNull()));
EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Idle));
EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle))));
PumpApplication();
EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Began));
EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle))));
PumpApplication();
EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Updated));
EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle))));
PumpApplication();
EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Ended));
EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle))));
PumpApplication();
EXPECT_THAT(activeButtonChannel->GetState(), Eq(InputChannel::State::Idle));
EXPECT_THAT(inactiveButtonChannels, Each(Property(&InputChannel::GetState, Eq(InputChannel::State::Idle))));
}
INSTANTIATE_TEST_CASE_P(
AllButtons,
XcbInputDeviceMouseButtonTests,
testing::Values(
MouseButtonTestData{ XCB_BUTTON_INDEX_1 },
MouseButtonTestData{ XCB_BUTTON_INDEX_2 },
MouseButtonTestData{ XCB_BUTTON_INDEX_3 }
// XCB_BUTTON_INDEX_4 and XCB_BUTTON_INDEX_5 map to positive and
// negative scroll wheel events, which are handled as motion events
)
);
TEST_F(XcbInputDeviceMouseTests, MovementInputChannelsUpdateStateFromXcbEvents)
{
using testing::Each;
using testing::Eq;
using testing::FloatEq;
using testing::NotNull;
using testing::Property;
using testing::Return;
// Set the expectations for the events that will be generated
// nullptr entries represent when the event queue is empty, and will cause
// PumpSystemEventLoopUntilEmpty to return
//
// Event pointers are freed by the calling code, so these actions
// malloc new copies
//
// The xcb mouse does not react to the `XCB_MOTION_NOTIFY` event, but
// it will still receive it from the X server.
EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(MakeEvent(xcb_input_raw_motion_event_t{
/*response_type=*/XCB_GE_GENERIC,
/*extension=*/s_xinputMajorOpcode,
/*sequence=*/5,
/*length=*/10,
/*event_type=*/XCB_INPUT_RAW_MOTION,
/*deviceid=*/s_virtualCorePointerId,
/*time=*/0, // use the time value to identify each event
/*detail=*/XCB_MOTION_NORMAL,
/*sourceid=*/s_physicalPointerDeviceId,
/*valuators_len=*/2, // number of axes that have values for this event
/*flags=*/0,
/*pad0[4]=*/{},
/*full_sequence=*/5,
})))
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(MakeEvent(xcb_motion_notify_event_t{
/*response_type=*/XCB_MOTION_NOTIFY,
/*detail=*/XCB_MOTION_NORMAL,
/*sequence=*/5,
/*time=*/1, // use the time value to identify each event
/*root=*/s_rootWindow,
/*event=*/127926272,
/*child=*/0,
/*root_x=*/95,
/*root_y=*/1079,
/*event_x=*/95,
/*event_y=*/20,
/*state=*/0,
/*same_screen=*/1,
})))
.WillOnce(Return(nullptr))
;
AZStd::array axisValues
{
xcb_input_fp3232_t{ /*.integral=*/ 1, /*.fraction=*/0 }, // x motion
xcb_input_fp3232_t{ /*.integral=*/ 2, /*.fraction=*/0 } // y motion
};
EXPECT_CALL(m_interface, xcb_input_raw_button_press_axisvalues_length(testing::Field(&xcb_input_raw_button_press_event_t::time, 0)))
.WillRepeatedly(testing::Return(2)); // x and y axis
EXPECT_CALL(m_interface, xcb_input_raw_button_press_axisvalues_raw(testing::Field(&xcb_input_raw_button_press_event_t::time, 0)))
.WillRepeatedly(testing::Return(axisValues.data())); // x and y axis
m_application.Start();
InputSystemCursorRequestBus::Event(
InputDeviceMouse::Id,
&InputSystemCursorRequests::SetSystemCursorState,
SystemCursorState::ConstrainedAndHidden);
const InputChannel* xMotionChannel = InputChannelRequests::FindInputChannel(InputDeviceMouse::Movement::X);
const InputChannel* yMotionChannel = InputChannelRequests::FindInputChannel(InputDeviceMouse::Movement::Y);
ASSERT_TRUE(xMotionChannel);
ASSERT_TRUE(yMotionChannel);
EXPECT_THAT(xMotionChannel->GetState(), Eq(InputChannel::State::Idle));
EXPECT_THAT(yMotionChannel->GetState(), Eq(InputChannel::State::Idle));
EXPECT_THAT(xMotionChannel->GetValue(), FloatEq(0.0f));
EXPECT_THAT(yMotionChannel->GetValue(), FloatEq(0.0f));
PumpApplication();
EXPECT_THAT(xMotionChannel->GetState(), Eq(InputChannel::State::Began));
EXPECT_THAT(yMotionChannel->GetState(), Eq(InputChannel::State::Began));
EXPECT_THAT(xMotionChannel->GetValue(), FloatEq(1.0f));
EXPECT_THAT(yMotionChannel->GetValue(), FloatEq(2.0f));
PumpApplication();
EXPECT_THAT(xMotionChannel->GetState(), Eq(InputChannel::State::Ended));
EXPECT_THAT(yMotionChannel->GetState(), Eq(InputChannel::State::Ended));
EXPECT_THAT(xMotionChannel->GetValue(), FloatEq(0.0f));
EXPECT_THAT(yMotionChannel->GetValue(), FloatEq(0.0f));
}
struct GetCursorPositionParam
{
int16_t m_x;
int16_t m_y;
};
class XcbGetSystemCursorPositionTests
: public XcbInputDeviceMouseTests
, public testing::WithParamInterface<GetCursorPositionParam>
{
};
TEST_P(XcbGetSystemCursorPositionTests, GetSystemCursorPositionNormalizedReturnsCorrectValue)
{
using testing::Eq;
using testing::Field;
using testing::Return;
using testing::_;
xcb_window_t focusWindow = 42;
const xcb_query_pointer_reply_t queryPointerReply{
/*.response_type=*/XCB_QUERY_POINTER,
/*.same_screen=*/1,
/*.sequence=*/0,
/*.length=*/1,
/*.root=*/s_rootWindow,
/*.child=*/focusWindow,
/*.root_x=*/static_cast<int16_t>(GetParam().m_x + s_defaultWindowGeometry.x),
/*.root_y=*/static_cast<int16_t>(GetParam().m_y + s_defaultWindowGeometry.y),
/*.win_x=*/GetParam().m_x,
/*.win_y=*/GetParam().m_y,
/*.mask=*/{},
/*.pad0[2]=*/{},
};
// Querying the root window's pointer gives its absolute value
const xcb_query_pointer_reply_t rootWindowQueryPointerReply{
/*.response_type=*/XCB_QUERY_POINTER,
/*.same_screen=*/1,
/*.sequence=*/0,
/*.length=*/1,
/*.root=*/s_rootWindow,
/*.child=*/s_rootWindow,
/*.root_x=*/static_cast<int16_t>(GetParam().m_x + s_defaultWindowGeometry.x),
/*.root_y=*/static_cast<int16_t>(GetParam().m_y + s_defaultWindowGeometry.y),
/*.win_x=*/static_cast<int16_t>(GetParam().m_x + s_defaultWindowGeometry.x),
/*.win_y=*/static_cast<int16_t>(GetParam().m_y + s_defaultWindowGeometry.y),
/*.mask=*/{},
/*.pad0[2]=*/{},
};
EXPECT_CALL(m_interface, xcb_get_property_value(Field(&xcb_get_property_reply_t::sequence, Eq(s_getActiveWindowPropertySequence))))
.WillRepeatedly(Return(&focusWindow));
EXPECT_CALL(m_interface, xcb_query_pointer(&m_connection, focusWindow))
.WillRepeatedly(Return(xcb_query_pointer_cookie_t{/*.sequence=*/1}));
EXPECT_CALL(m_interface, xcb_query_pointer_reply(&m_connection, Field(&xcb_query_pointer_cookie_t::sequence, 1), _))
.WillRepeatedly(ReturnMalloc<xcb_query_pointer_reply_t>(queryPointerReply));
EXPECT_CALL(m_interface, xcb_query_pointer(&m_connection, s_rootWindow))
.WillRepeatedly(Return(xcb_query_pointer_cookie_t{/*.sequence=*/2}));
EXPECT_CALL(m_interface, xcb_query_pointer_reply(&m_connection, Field(&xcb_query_pointer_cookie_t::sequence, 2), _))
.WillRepeatedly(ReturnMalloc<xcb_query_pointer_reply_t>(rootWindowQueryPointerReply));
m_application.Start();
InputSystemCursorRequestBus::Event(
InputDeviceMouse::Id,
&InputSystemCursorRequests::SetSystemCursorState,
SystemCursorState::ConstrainedAndHidden);
AZ::Vector2 systemCursorPositionNormalized = AZ::Vector2::CreateZero();
InputSystemCursorRequestBus::EventResult(
systemCursorPositionNormalized,
InputDeviceMouse::Id,
&InputSystemCursorRequests::GetSystemCursorPositionNormalized);
EXPECT_THAT(systemCursorPositionNormalized, ::testing::AllOf(
testing::Property(&AZ::Vector2::GetX, testing::FloatEq(static_cast<float>(GetParam().m_x) / s_defaultWindowGeometry.width)),
testing::Property(&AZ::Vector2::GetY, testing::FloatEq(static_cast<float>(GetParam().m_y) / s_defaultWindowGeometry.height))
));
}
INSTANTIATE_TEST_CASE_P(
AllPointerPositions,
XcbGetSystemCursorPositionTests,
testing::Values(
// Default mocked window geometry sets width and height to 100, all
// parameter values should be within [0, 100)
GetCursorPositionParam{ 50, 50 },
GetCursorPositionParam{ 25, 25 },
GetCursorPositionParam{ 0, 100 }
)
);
} // namespace AzFramework
@@ -17,5 +17,6 @@ set(FILES
XcbBaseTestFixture.cpp
XcbBaseTestFixture.h
XcbInputDeviceKeyboardTests.cpp
XcbInputDeviceMouseTests.cpp
XcbTestApplication.h
)
@@ -55,6 +55,40 @@ namespace UnitTest
AZ::EntityId m_entityReference;
};
class SourceSpawnableComponent : public AZ::Component
{
public:
AZ_COMPONENT(SourceSpawnableComponent, "{47FF79CE-A95B-420E-8BEB-F1CC58087B87}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<SourceSpawnableComponent, AZ::Component>();
}
}
};
class TargetSpawnableComponent : public AZ::Component
{
public:
AZ_COMPONENT(TargetSpawnableComponent, "{B4041561-63A7-4E1E-80F1-78C08D497960}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<TargetSpawnableComponent, AZ::Component>();
}
}
};
class SpawnableEntitiesManagerTest : public AllocatorsFixture
{
public:
@@ -66,6 +100,8 @@ namespace UnitTest
AZ::ComponentApplication::Descriptor descriptor;
m_application->Start(descriptor);
m_application->RegisterComponentDescriptor(ComponentWithEntityReference::CreateDescriptor());
m_application->RegisterComponentDescriptor(SourceSpawnableComponent::CreateDescriptor());
m_application->RegisterComponentDescriptor(TargetSpawnableComponent::CreateDescriptor());
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
@@ -109,10 +145,126 @@ namespace UnitTest
entities.reserve(numElements);
for (size_t i=0; i<numElements; ++i)
{
entities.push_back(AZStd::make_unique<AZ::Entity>());
auto entry = AZStd::make_unique<AZ::Entity>();
entry->AddComponent(aznew SourceSpawnableComponent());
entities.push_back(AZStd::move(entry));
}
}
AZ::Data::Asset<AzFramework::Spawnable> CreateTargetSpawnable(size_t numElements)
{
auto target = aznew AzFramework::Spawnable(
AZ::Data::AssetId(AZ::Uuid("{716CD8C3-0BA8-4F32-B579-0EC7C967796F}")), AZ::Data::AssetData::AssetStatus::Ready);
AzFramework::Spawnable::EntityList& entities = target->GetEntities();
entities.reserve(numElements);
for (size_t i = 0; i < numElements; ++i)
{
auto entry = AZStd::make_unique<AZ::Entity>();
entry->AddComponent(aznew TargetSpawnableComponent());
entities.push_back(AZStd::move(entry));
}
return AZ::Data::Asset<AzFramework::Spawnable>(target, AZ::Data::AssetLoadBehavior::NoLoad);
}
template<size_t AliasCount>
void InsertEntityAliases(
const AZStd::array<uint32_t, AliasCount>& sourceIds,
const AZStd::array<uint32_t, AliasCount>& targetIds,
const AZStd::array<AzFramework::Spawnable::EntityAliasType, AliasCount>& aliasTypes,
AZ::Data::Asset<AzFramework::Spawnable>* target = nullptr)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
for (uint32_t i = 0; i < AliasCount; ++i)
{
if (target)
{
visitor.AddAlias(*target, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false);
}
else
{
AZ::Data::Asset<AzFramework::Spawnable> spawnable(
AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid<AzFramework::Spawnable>());
visitor.AddAlias(AZStd::move(spawnable), AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false);
}
}
}
static bool AreAllEntitiesReplaced(AzFramework::SpawnableConstEntityContainerView entities)
{
for (const AZ::Entity* entity : entities)
{
if (entity)
{
if (entity->FindComponent<SourceSpawnableComponent>() != nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == nullptr)
{
return false;
}
}
else
{
return false;
}
}
return true;
}
static bool IsEveryOtherEntityAReplacement(AzFramework::SpawnableConstEntityContainerView entities)
{
bool onAlternative = true;
for (const AZ::Entity* entity : entities)
{
if (entity)
{
if (onAlternative)
{
if (entity->FindComponent<SourceSpawnableComponent>() == nullptr ||
entity->FindComponent<TargetSpawnableComponent>() != nullptr)
{
return false;
}
}
else
{
if (entity->FindComponent<SourceSpawnableComponent>() != nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == nullptr)
{
return false;
}
}
onAlternative = !onAlternative;
}
else
{
return false;
}
}
return true;
}
static bool AreAllMerged(AzFramework::SpawnableConstEntityContainerView entities)
{
for (const AZ::Entity* entity : entities)
{
if (entity)
{
if (entity->FindComponent<SourceSpawnableComponent>() == nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == nullptr)
{
return false;
}
}
else
{
return false;
}
}
return true;
}
void CreateRecursiveHierarchy()
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
@@ -245,6 +397,30 @@ namespace UnitTest
TestApplication* m_application { nullptr };
};
//
// Constructors
//
TEST_F(SpawnableEntitiesManagerTest, EntitySpawnTicket_Move_Works)
{
AzFramework::EntitySpawnTicket ticket1(*m_spawnableAsset);
AzFramework::EntitySpawnTicket ticket2(*m_spawnableAsset);
const AzFramework::EntitySpawnTicket::Id ticket1Id = ticket1.GetId();
const AzFramework::EntitySpawnTicket::Id ticket2Id = ticket2.GetId();
AzFramework::EntitySpawnTicket ticketMoveConstructor(AZStd::move(ticket1));
EXPECT_TRUE(ticketMoveConstructor.IsValid());
EXPECT_EQ(ticketMoveConstructor.GetId(), ticket1Id);
AzFramework::EntitySpawnTicket ticketMoveOperator;
ticketMoveOperator = AZStd::move(ticket2);
EXPECT_TRUE(ticketMoveOperator.IsValid());
EXPECT_EQ(ticketMoveOperator.GetId(), ticket2Id);
}
//
// SpawnAllEntitities
//
@@ -366,24 +542,6 @@ namespace UnitTest
}
}
TEST_F(SpawnableEntitiesManagerTest, EntitySpawnTicket_Move_Works)
{
AzFramework::EntitySpawnTicket ticket1(*m_spawnableAsset);
AzFramework::EntitySpawnTicket ticket2(*m_spawnableAsset);
const AzFramework::EntitySpawnTicket::Id ticket1Id = ticket1.GetId();
const AzFramework::EntitySpawnTicket::Id ticket2Id = ticket2.GetId();
AzFramework::EntitySpawnTicket ticketMoveConstructor(AZStd::move(ticket1));
EXPECT_TRUE(ticketMoveConstructor.IsValid());
EXPECT_EQ(ticketMoveConstructor.GetId(), ticket1Id);
AzFramework::EntitySpawnTicket ticketMoveOperator;
ticketMoveOperator = AZStd::move(ticket2);
EXPECT_TRUE(ticketMoveOperator.IsValid());
EXPECT_EQ(ticketMoveOperator.GetId(), ticket2Id);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{
{
@@ -393,6 +551,135 @@ namespace UnitTest
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithDisabled_NoEntitiesSpawned)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
InsertEntityAliases<NumEntities>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Disable });
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(0, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_SomeAliasesWithDisabled_RegularEntitiesAreSpawned)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 8;
FillSpawnable(NumEntities);
InsertEntityAliases<2>({ 1, 3 }, { 1, 3 }, { Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable });
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(6, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithReplace_EntitiesSpawnedFromTarget)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace },
&target);
size_t spawnedEntitiesCount = 0;
bool allReplaced = false;
auto callback = [&spawnedEntitiesCount, &allReplaced](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allReplaced = AreAllEntitiesReplaced(entities);
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(4, spawnedEntitiesCount);
EXPECT_TRUE(allReplaced);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithAdditional_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional,
Spawnable::EntityAliasType::Additional },
&target);
size_t spawnedEntitiesCount = 0;
bool allAdded = false;
auto callback = [&spawnedEntitiesCount, &allAdded](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allAdded = IsEveryOtherEntityAReplacement(entities);
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(8, spawnedEntitiesCount);
EXPECT_TRUE(allAdded);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge,
Spawnable::EntityAliasType::Merge },
&target);
size_t spawnedEntitiesCount = 0;
bool allMerged = false;
auto callback = [&spawnedEntitiesCount, &allMerged](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allMerged = AreAllMerged(entities);
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(4, spawnedEntitiesCount);
EXPECT_TRUE(allMerged);
}
//
// SpawnEntities
@@ -403,7 +690,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
@@ -423,7 +710,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 1;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 0 };
AZStd::vector<uint32_t> indices = { 0, 0 };
size_t spawnedEntitiesCount = 0;
auto callback =
@@ -444,7 +731,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback =
@@ -467,7 +754,7 @@ namespace UnitTest
FillSpawnable(NumEntities);
CreateSingleParent();
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
AZStd::vector<uint32_t> indices = { 0, 1, 2, 3 };
AZStd::vector<AZ::EntityId> parents;
auto callback = [&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
@@ -499,7 +786,7 @@ namespace UnitTest
FillSpawnable(NumEntities);
CreateSingleParent();
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
AZStd::vector<uint32_t> indices = { 0, 1, 2, 3 };
AZStd::vector<AZ::EntityId> parents;
auto callback =
@@ -754,6 +1041,148 @@ namespace UnitTest
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithDisabled_NoEntitiesSpawned)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
InsertEntityAliases<NumEntities>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Disable });
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(0, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_SomeAliasesWithDisabled_RegularEntitiesAreSpawned)
{
using namespace AzFramework;
FillSpawnable(8);
InsertEntityAliases<3>(
{ 1, 3, 6 }, { 1, 3, 6 },
{ Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable });
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1, 2, 3, 0, 1, 6, 4, 5, 7, 4, 1, 0, 6 };
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(9, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithReplace_EntitiesSpawnedFromTarget)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace },
&target);
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allReplaced = false;
auto callback = [&spawnedEntitiesCount, &allReplaced](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allReplaced = AreAllEntitiesReplaced(entities);
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(4, spawnedEntitiesCount);
EXPECT_TRUE(allReplaced);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithAdditional_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional,
Spawnable::EntityAliasType::Additional },
&target);
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allAdded = false;
auto callback =
[&spawnedEntitiesCount, &allAdded](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allAdded = IsEveryOtherEntityAReplacement(entities);
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(8, spawnedEntitiesCount);
EXPECT_TRUE(allAdded);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge,
Spawnable::EntityAliasType::Merge },
&target);
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allMerged = false;
auto callback = [&spawnedEntitiesCount, &allMerged](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allMerged = AreAllMerged(entities);
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(4, spawnedEntitiesCount);
EXPECT_TRUE(allMerged);
}
//
// DespawnAllEntities
@@ -0,0 +1,513 @@
/*
* 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/Casting/numeric_cast.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzTest/AzTest.h>
namespace UnitTest
{
class SpawnableTest : public AllocatorsFixture
{
public:
static constexpr size_t DefaultEntityAliasTestCount = 8;
void SetUp() override
{
AllocatorsFixture::SetUp();
m_spawnable = aznew AzFramework::Spawnable();
}
void TearDown() override
{
delete m_spawnable;
m_spawnable = nullptr;
AllocatorsFixture::TearDown();
}
void InsertEntities(size_t count)
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
entities.reserve(entities.size() + count);
for (size_t i = 0; i < count; ++i)
{
entities.emplace_back(AZStd::make_unique<AZ::Entity>());
}
}
template<size_t Count>
void InsertEntityAliases(
const AZStd::array<uint32_t, Count>& sourceIds,
const AZStd::array<uint32_t, Count>& targetIds,
const AZStd::array<AzFramework::Spawnable::EntityAliasType, Count>& aliasTypes,
bool queueLoad = false)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
for (uint32_t i = 0; i < Count; ++i)
{
AZ::Data::Asset<AzFramework::Spawnable> spawnable(
AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid<AzFramework::Spawnable>());
visitor.AddAlias(spawnable, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], queueLoad);
}
}
template<size_t Count>
void InsertEntityAliases(bool queueLoad)
{
using namespace AzFramework;
AZStd::array<uint32_t, Count> ids;
for (uint32_t i=0; i<aznumeric_cast<uint32_t>(Count); ++i)
{
ids[i] = i;
}
AZStd::array<AzFramework::Spawnable::EntityAliasType, Count> aliasTypes;
for (uint32_t i = 0; i < aznumeric_cast<uint32_t>(Count); ++i)
{
aliasTypes[i] = Spawnable::EntityAliasType::Replace;
}
InsertEntityAliases<Count>(ids, ids, aliasTypes, queueLoad);
}
template<size_t Count>
void InsertEntityAliases()
{
InsertEntityAliases<Count>(false);
}
protected:
AzFramework::Spawnable* m_spawnable;
};
//
// TryGetAliasesConst
//
TEST_F(SpawnableTest, TryGetAliasesConst_GetVisitor_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_TRUE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsNotReadShared_VisitorDataIsNotAvailable)
{
AzFramework::Spawnable::EntityAliasVisitor readWriteVisitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(readWriteVisitor.IsValid());
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_FALSE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyReadShared_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst();
ASSERT_TRUE(readVisitor.IsValid());
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_TRUE(visitor.IsValid());
}
//
// TryGetAliases
//
TEST_F(SpawnableTest, TryGetAliases_GetVisitor_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
EXPECT_TRUE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyShared_VisitorDataNotIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst();
ASSERT_TRUE(readVisitor.IsValid());
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
EXPECT_FALSE(visitor.IsValid());
}
//
// EntityAliasVisitor
//
//
// HasAliases
//
TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_EmptyAliasList_ReturnsFalse)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_FALSE(visitor.HasAliases());
}
TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_FilledInAliasList_ReturnsTrue)
{
InsertEntities(8);
InsertEntityAliases<8>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_TRUE(visitor.HasAliases());
}
//
// Optimize
//
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_SortEntityAliases_AliasesAreSortedBySourceAndTargetId)
{
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
// Optimize doesn't need to be explicitly called because the setup of the aliases will cause the alias list to be sorted and optimized.
uint32_t sourceIndex = 0;
uint32_t targetIndex = 0;
for (const AzFramework::Spawnable::EntityAlias& alias : visitor)
{
if (alias.m_sourceIndex != sourceIndex)
{
ASSERT_LE(sourceIndex, alias.m_sourceIndex);
}
else
{
ASSERT_LE(targetIndex, alias.m_targetIndex);
}
sourceIndex = alias.m_sourceIndex;
targetIndex = alias.m_targetIndex;
}
}
TEST_F(
SpawnableTest, EntityAliasVisitor_Optimize_RemoveUnused_OnlySecondToLastAliasRemains)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(1, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()->m_aliasType);
EXPECT_EQ(6, visitor.begin()->m_targetIndex);
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_AddAdditional_ThreeAdditionalAliasesAreAdded)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 0, 0, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional,
Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional,
Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(11, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[5].m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[7].m_aliasType);
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_OriginalsOnly_AliasListIsEmpty)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end()));
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MixedOriginals_AllOriginalsRemoved)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 0, 1, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(2, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Disable, visitor.begin()->m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[1].m_aliasType);
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MergeAfterOriginal_NoAdditionalOriginalIsInserted)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 1, 1, 2, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(4, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Merge, visitor.begin()[1].m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[2].m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Merge, visitor.begin()[3].m_aliasType);
}
//
// UpdateAliasType
//
TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliasType_AllToOriginal_NoAliasesAfterOptimization)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
for (uint32_t i = 0; i < 8; ++i)
{
visitor.UpdateAliasType(i, Spawnable::EntityAliasType::Original);
}
for (const Spawnable::EntityAlias& alias : visitor)
{
EXPECT_EQ(Spawnable::EntityAliasType::Original, alias.m_aliasType);
}
visitor.Optimize();
EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end()));
}
//
// UpdateAliases
//
TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_AllToOriginal_NoAliasesAfterOptimization)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
auto callback =
[](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/, const AZ::Data::Asset<Spawnable>& /*aliasedSpawnable*/,
const AZ::Crc32 /*tag*/, const uint32_t /*sourceIndex*/, const uint32_t /*targetIndex*/)
{
aliasType = Spawnable::EntityAliasType::Original;
};
visitor.UpdateAliases(AZStd::move(callback));
for (const Spawnable::EntityAlias& alias : visitor)
{
EXPECT_EQ(Spawnable::EntityAliasType::Original, alias.m_aliasType);
}
visitor.Optimize();
EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end()));
}
TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_FilterByTag_OnlyOneAliasUpdated)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
bool correctTag = false;
size_t numberOfUpdates = 0;
auto callback = [&correctTag, &numberOfUpdates](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/,
const AZ::Data::Asset<Spawnable>& /*aliasedSpawnable*/, const AZ::Crc32 tag, const uint32_t /*sourceIndex*/,
const uint32_t /*targetIndex*/)
{
correctTag = (tag == AZ::Crc32(3));
numberOfUpdates++;
aliasType = Spawnable::EntityAliasType::Original;
};
visitor.UpdateAliases(AZ::Crc32(3), AZStd::move(callback));
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[3].m_aliasType);
}
//
// AreAllSpawnablesReady
//
TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeLoadedAssets_ReturnsTrue)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_TRUE(visitor.AreAllSpawnablesReady());
}
TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeNotLoadedAssets_ReturnsFalse)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(true);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_FALSE(visitor.AreAllSpawnablesReady());
}
//
// ListTargetSpawnables
//
TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListAllTargetAssets_AllTargetsListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAssets = true;
auto callback = [&count, &correctAssets](const AZ::Data::Asset<Spawnable>& targetSpawnable)
{
correctAssets = correctAssets && (targetSpawnable.GetId().m_subId == count);
count++;
};
visitor.ListTargetSpawnables(callback);
EXPECT_EQ(8, count);
EXPECT_TRUE(correctAssets);
}
TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListTaggedTargetAssets_OneAssetListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAsset = false;
auto callback = [&count, &correctAsset](const AZ::Data::Asset<Spawnable>& targetSpawnable)
{
correctAsset = (targetSpawnable.GetId().m_subId == 3);
count++;
};
visitor.ListTargetSpawnables(AZ::Crc32(3), callback);
EXPECT_EQ(1, count);
EXPECT_TRUE(correctAsset);
}
//
// ListSpawnablesRequiringLoad
//
TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToLoaded_AllTargetsListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>(true);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAssets = true;
auto callback = [&count, &correctAssets](const AZ::Data::Asset<Spawnable>& targetSpawnable)
{
correctAssets = correctAssets && (targetSpawnable.GetId().m_subId == count);
count++;
};
visitor.ListSpawnablesRequiringLoad(callback);
EXPECT_EQ(8, count);
EXPECT_TRUE(correctAssets);
}
TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToNotLoaded_NoTargetsListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>(false);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
auto callback = [&count](const AZ::Data::Asset<Spawnable>& /*targetSpawnable*/)
{
count++;
};
visitor.ListSpawnablesRequiringLoad(callback);
EXPECT_EQ(0, count);
}
} // namespace UnitTest
@@ -10,6 +10,7 @@ set(FILES
Main.cpp
Spawnable/SpawnableEntitiesInterfaceTests.cpp
Spawnable/SpawnableEntitiesManagerTests.cpp
Spawnable/SpawnableTests.cpp
ArchiveCompressionTests.cpp
ArchiveTests.cpp
BehaviorEntityTests.cpp
@@ -87,7 +87,7 @@ namespace AzGameFramework
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_ProjectRegistry(registry, AZ_TRAIT_OS_PLATFORM_CODENAME, specializations, &scratchBuffer);
#endif
// Used the lowercase the platform name since the bootstrap.game.<config>.<platform>.setreg is being loaded
// Used the lowercase the platform name since the bootstrap.game.<config>.setreg is being loaded
// from the asset cache root where all the files are in lowercased from regardless of the filesystem case-sensitivity
static constexpr char filename[] = "bootstrap.game." AZ_BUILD_CONFIGURATION_TYPE ".setreg";
@@ -13,7 +13,9 @@
<Member Type="AzNetworking::DisconnectReason" Name="disconnectReason" Init="AzNetworking::DisconnectReason::None" />
</Packet>
<Packet Name="HeartbeatPacket" Desc="This packet is used to keep an established connection alive" />
<Packet Name="HeartbeatPacket" Desc="This packet is used to keep an established connection alive">
<Member Type="bool" Name="requestResponse" Init="false" />
</Packet>
<Packet Name="FragmentedPacket" Desc="This packet is used to segment a packet that exceeds a connections MTU">
<Member Type="AzNetworking::SequenceId" Name="unfragmentedSequence" Init="AzNetworking::InvalidSequenceId" />
@@ -122,10 +122,4 @@ namespace AzNetworking
m_timeoutItemMap.erase(itemTimeoutId);
}
}
void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts)
{
TimeoutHandler handler([&timeoutHandler](TimeoutQueue::TimeoutItem& item) { return timeoutHandler.HandleTimeout(item); });
UpdateTimeouts(handler, maxTimeouts);
}
}
@@ -23,8 +23,6 @@ namespace AzNetworking
Delete
};
class ITimeoutHandler;
//! @class TimeoutQueue
//! @brief class for managing timeout items.
class TimeoutQueue
@@ -70,11 +68,6 @@ namespace AzNetworking
using TimeoutHandler = AZStd::function<TimeoutResult(TimeoutQueue::TimeoutItem&)>;
void UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts = -1);
//! Updates timeouts for all items, invokes timeout handlers if required.
//! @param timeoutHandler listener instance to call back on for timeouts
//! @param maxTimeouts the maximum number of timeouts to process before breaking iteration
void UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts = -1);
private:
struct TimeoutQueueItem
@@ -94,19 +87,6 @@ namespace AzNetworking
TimeoutItemMap m_timeoutItemMap;
TimeoutItemQueue m_timeoutItemQueue;
};
//! @class ITimeoutHandler
//! @brief interface class for managing timeout items.
class ITimeoutHandler
{
public:
virtual ~ITimeoutHandler() = default;
//! Handler callback for timed out items.
//! @param item containing registered timeout details
//! @return ETimeoutResult for whether to re-register or discard the timeout params
virtual TimeoutResult HandleTimeout(TimeoutQueue::TimeoutItem& item) = 0;
};
}
#include <AzNetworking/DataStructures/TimeoutQueue.inl>
@@ -27,13 +27,11 @@ namespace AzNetworking
ConnectionId connectionId,
const IpAddress& remoteAddress,
TcpNetworkInterface& networkInterface,
TcpSocket& socket,
TimeoutId timeoutId
TcpSocket& socket
)
: IConnection(connectionId, remoteAddress)
, m_networkInterface(networkInterface)
, m_socket(socket.CloneAndTakeOwnership())
, m_timeoutId(timeoutId)
, m_state(m_socket->IsOpen() ? ConnectionState::Connecting : ConnectionState::Disconnected)
, m_connectionRole(ConnectionRole::Acceptor)
, m_registeredSocketFd(InvalidSocketFd)
@@ -163,13 +161,6 @@ namespace AzNetworking
break;
}
TimeoutQueue::TimeoutItem* timeoutItem = m_networkInterface.m_connectionTimeoutQueue.RetrieveItem(GetTimeoutId());
if (timeoutItem == nullptr)
{
return true;
}
timeoutItem->UpdateTimeoutTime(startTimeMs);
NetworkOutputSerializer serializer(buffer.GetBuffer(), static_cast<uint32_t>(buffer.GetSize()));
if (m_state == ConnectionState::Connecting)
{
@@ -38,14 +38,12 @@ namespace AzNetworking
//! @param remoteAddress IP address of the remote endpoint
//! @param networkInterface TcpNetworkInterface that owns this connection instance
//! @param socket TCP socket to take ownership of and use for sending and receiving data
//! @param timeoutId timeout identifier of this connection instance
TcpConnection
(
ConnectionId connectionId,
const IpAddress& remoteAddress,
TcpNetworkInterface& networkInterface,
TcpSocket& socket,
TimeoutId timeoutId
TcpSocket& socket
);
//! Construct a new socket with optional encryption, used when initiating a new connection
@@ -69,14 +67,6 @@ namespace AzNetworking
//! @return the TcpSocket bound to this TcpConnection
TcpSocket* GetTcpSocket() const;
//! Sets the timeout identifier for this TcpConnection.
//! @param timeoutId the timeout identifier to use for this TcpConnection
void SetTimeoutId(TimeoutId timeoutId);
//! Returns the timeout identifier for this TcpConnection.
//! @return the timeout identifier for this TcpConnection
TimeoutId GetTimeoutId() const;
//! Returns true if this connection instance is in an open state, and is capable of actively sending and receiving packets.
//! @return boolean true if this connection instance is in an open state
bool IsOpen() const;
@@ -142,7 +132,6 @@ namespace AzNetworking
AZStd::unique_ptr<TcpSocket> m_socket;
AZStd::unique_ptr<ICompressor> m_compressor;
TimeoutId m_timeoutId;
PacketId m_lastSentPacketId = InvalidPacketId;
ConnectionState m_state = ConnectionState::Disconnected;
ConnectionRole m_connectionRole = ConnectionRole::Connector;
@@ -15,16 +15,6 @@ namespace AzNetworking
return m_socket.get();
}
inline void TcpConnection::SetTimeoutId(TimeoutId timeoutId)
{
m_timeoutId = timeoutId;
}
inline TimeoutId TcpConnection::GetTimeoutId() const
{
return m_timeoutId;
}
inline bool TcpConnection::IsOpen() const
{
return m_socket->IsOpen();
@@ -21,16 +21,11 @@ namespace AzNetworking
static const bool net_TcpUseEncryption = false;
#endif
AZ_CVAR(bool, net_TcpTimeoutConnections, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Boolean value on whether we should timeout Tcp connections");
AZ_CVAR(AZ::TimeMs, net_TcpHeartbeatTimeMs, AZ::TimeMs{ 2 * 1000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Tcp connection heartbeat frequency");
AZ_CVAR(AZ::TimeMs, net_TcpDefaultTimeoutMs, AZ::TimeMs{ 10 * 1000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Time in milliseconds before we timeout an idle Tcp connection");
TcpNetworkInterface::TcpNetworkInterface(AZ::Name name, IConnectionListener& connectionListener, TrustZone trustZone, TcpListenThread& listenThread)
: m_name(name)
, m_trustZone(trustZone)
, m_connectionListener(connectionListener)
, m_listenThread(listenThread)
, m_timeoutMs(net_TcpDefaultTimeoutMs)
{
;
}
@@ -98,8 +93,6 @@ namespace AzNetworking
}
AZLOG_INFO("Adding new socket %d", static_cast<int32_t>(tcpSocket->GetSocketFd()));
const TimeoutId newTimeoutId = m_connectionTimeoutQueue.RegisterItem(static_cast<uint64_t>(tcpSocket->GetSocketFd()), net_TcpHeartbeatTimeMs);
connection->SetTimeoutId(newTimeoutId);
connection->SendReliablePacket(CorePackets::InitiateConnectionPacket());
m_connectionListener.OnConnect(connection.get());
m_connectionSet.AddConnection(AZStd::move(connection));
@@ -110,12 +103,6 @@ namespace AzNetworking
{
const AZ::TimeMs startTimeMs = AZ::GetElapsedTimeMs();
// Time out any stale connections
{
ConnectionTimeoutFunctor functor(*this);
m_connectionTimeoutQueue.UpdateTimeouts(functor);
}
AcceptNewConnections();
auto readCallback = [this, startTimeMs](SocketFd socketFd) { HandleConnectionRecv(socketFd, startTimeMs); };
@@ -258,8 +245,7 @@ namespace AzNetworking
return;
}
AZLOG(NET_TcpTraffic, "Adding new socket %d", static_cast<int32_t>(tcpSocket.GetSocketFd()));
const TimeoutId timeoutId = m_connectionTimeoutQueue.RegisterItem(static_cast<uint64_t>(tcpSocket.GetSocketFd()), m_timeoutMs);
AZStd::unique_ptr<TcpConnection> connection = AZStd::make_unique<TcpConnection>(connectionId, remoteAddress, *this, tcpSocket, timeoutId);
AZStd::unique_ptr<TcpConnection> connection = AZStd::make_unique<TcpConnection>(connectionId, remoteAddress, *this, tcpSocket);
AZ_Assert(connection->GetConnectionRole() == ConnectionRole::Acceptor, "Invalid role for connection");
GetConnectionListener().OnConnect(connection.get());
m_connectionSet.AddConnection(AZStd::move(connection));
@@ -286,7 +272,6 @@ namespace AzNetworking
m_pendingRemoves.resize_no_construct(0);
}
TcpNetworkInterface::PendingConnection::PendingConnection(SocketFd socketFd, uint32_t remoteIpAddress, uint16_t remotePort, uint16_t listenPort)
: m_socketFd(socketFd)
, m_remoteIpAddress(remoteIpAddress)
@@ -295,34 +280,4 @@ namespace AzNetworking
{
;
}
TcpNetworkInterface::ConnectionTimeoutFunctor::ConnectionTimeoutFunctor(TcpNetworkInterface& networkInterface)
: m_networkInterface(networkInterface)
{
;
}
TimeoutResult TcpNetworkInterface::ConnectionTimeoutFunctor::HandleTimeout(TimeoutQueue::TimeoutItem& item)
{
const SocketFd socketFd = static_cast<SocketFd>(item.m_userData);
TcpConnection* tcpConnection = m_networkInterface.m_connectionSet.GetConnection(socketFd);
if (tcpConnection == nullptr)
{
// We've already deleted this connection
return TimeoutResult::Delete;
}
if (tcpConnection->GetConnectionRole() == ConnectionRole::Connector)
{
tcpConnection->SendReliablePacket(CorePackets::HeartbeatPacket());
}
else if (net_TcpTimeoutConnections && (m_networkInterface.GetTimeoutMs() > AZ::TimeMs{ 0 }))
{
tcpConnection->Disconnect(DisconnectReason::Timeout, TerminationEndpoint::Local);
return TimeoutResult::Delete;
}
return TimeoutResult::Refresh;
}
}
@@ -137,16 +137,6 @@ namespace AzNetworking
AZ_DISABLE_COPY_MOVE(TcpNetworkInterface);
struct ConnectionTimeoutFunctor final
: public ITimeoutHandler
{
ConnectionTimeoutFunctor(TcpNetworkInterface& networkInterface);
TimeoutResult HandleTimeout(TimeoutQueue::TimeoutItem& item) override;
private:
AZ_DISABLE_COPY_MOVE(ConnectionTimeoutFunctor);
TcpNetworkInterface& m_networkInterface;
};
struct PendingRemove
{
SocketFd m_socketFd;
@@ -162,7 +152,6 @@ namespace AzNetworking
TcpSocketManager m_tcpSocketManager;
AZ::ThreadSafeDeque<PendingConnection> m_pendingConnections;
AZStd::vector<PendingRemove> m_pendingRemoves;
TimeoutQueue m_connectionTimeoutQueue;
TcpListenThread& m_listenThread;
friend class TcpConnection; // For access to private RequestDisconnect() method
@@ -79,7 +79,8 @@ namespace AzNetworking
AZLOG(NET_Acks, "Unacked packet count exceeded, sending client heartbeat (curr %u : max %u)", m_unackedPacketCount, static_cast<uint32_t>(net_UdpMaxUnackedPacketCount));
// This simply times out unreliable chunks that haven't completed within our timeout delay
m_fragmentQueue.Update();
SendUnreliablePacket(CorePackets::HeartbeatPacket());
// This heartbeat is sent to minimize the time the remote endpoint spends waiting for ack vector replication, we don't require a response
SendUnreliablePacket(CorePackets::HeartbeatPacket(false));
}
}
@@ -289,7 +290,11 @@ namespace AzNetworking
{
return PacketDispatchResult::Failure;
}
// Do nothing, we've already processed our ack packets
if (packet.GetRequestResponse())
{
// We're replying to a heartbeat request, we don't want a response
SendUnreliablePacket(CorePackets::HeartbeatPacket(false));
}
return PacketDispatchResult::Success;
}
break;
@@ -136,12 +136,12 @@ namespace AzNetworking
AZ_DISABLE_COPY_MOVE(UdpConnection);
UdpNetworkInterface& m_networkInterface;
UdpPacketTracker m_packetTracker;
UdpReliableQueue m_reliableQueue;
UdpFragmentQueue m_fragmentQueue;
ConnectionState m_state = ConnectionState::Disconnected;
ConnectionRole m_connectionRole = ConnectionRole::Connector;
DtlsEndpoint m_dtlsEndpoint;
UdpPacketTracker m_packetTracker;
UdpReliableQueue m_reliableQueue;
UdpFragmentQueue m_fragmentQueue;
ConnectionState m_state = ConnectionState::Disconnected;
ConnectionRole m_connectionRole = ConnectionRole::Connector;
DtlsEndpoint m_dtlsEndpoint;
AZ::TimeMs m_lastSentPacketMs;
uint32_t m_unackedPacketCount = 0;
@@ -20,7 +20,13 @@ namespace AzNetworking
void UdpFragmentQueue::Update()
{
m_timeoutQueue.UpdateTimeouts(*this);
m_timeoutQueue.UpdateTimeouts([this](TimeoutQueue::TimeoutItem& item)
{
const SequenceId fragmentSequence = static_cast<SequenceId>(item.m_userData & 0xFF);
AZLOG(NET_FragmentQueue, "Timing out unreliable fragmented packet %u", static_cast<uint32_t>(fragmentSequence));
m_packetFragments.erase(fragmentSequence);
return TimeoutResult::Delete;
});
}
void UdpFragmentQueue::Reset()
@@ -163,12 +169,4 @@ namespace AzNetworking
return handledPacket;
}
TimeoutResult UdpFragmentQueue::HandleTimeout(TimeoutQueue::TimeoutItem& item)
{
const SequenceId fragmentSequence = static_cast<SequenceId>(item.m_userData & 0xFF);
AZLOG(NET_FragmentQueue, "Timing out unreliable fragmented packet %u", static_cast<uint32_t>(fragmentSequence));
m_packetFragments.erase(fragmentSequence);
return TimeoutResult::Delete;
}
}
@@ -26,7 +26,6 @@ namespace AzNetworking
//! @class UdpFragmentQueue
//! @brief Class for reconstructing packet chunks into the original unsegmented packet.
class UdpFragmentQueue
: public ITimeoutHandler
{
public:
@@ -51,11 +50,6 @@ namespace AzNetworking
private:
//! Handler callback for timed out items.
//! @param item containing registered timeout details
//! @return ETimeoutResult for whether to re-register or discard the timeout params
TimeoutResult HandleTimeout(TimeoutQueue::TimeoutItem& item) override;
TimeoutQueue m_timeoutQueue;
SequenceGenerator m_sequenceGenerator;
@@ -31,7 +31,7 @@ namespace AzNetworking
AZ_CVAR(bool, net_UdpTimeoutConnections, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Boolean value on whether we should timeout Udp connections");
AZ_CVAR(AZ::TimeMs, net_UdpPacketTimeSliceMs, AZ::TimeMs{ 8 }, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The number of milliseconds to allow for packet processing");
AZ_CVAR(AZ::TimeMs, net_UdpHeartbeatTimeMs, AZ::TimeMs{ 2 * 1000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Udp connection heartbeat frequency");
AZ_CVAR(uint32_t, net_UdpUnackedHeartbeats, 5, nullptr, AZ::ConsoleFunctorFlags::Null, "The number of heartbeats to attempt to send to keep a connection alive before giving up");
AZ_CVAR(AZ::TimeMs, net_UdpDefaultTimeoutMs, AZ::TimeMs{ 10 * 1000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Time in milliseconds before we timeout an idle Udp connection");
AZ_CVAR(AZ::TimeMs, net_MinPacketTimeoutMs, AZ::TimeMs{ 200 }, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Minimum time to wait before timing out an unacked packet");
AZ_CVAR(int32_t, net_MaxTimeoutsPerFrame, 1000, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Maximum number of packet timeouts to allow to process in a single frame");
@@ -139,7 +139,8 @@ namespace AzNetworking
}
const ConnectionId connectionId = m_connectionSet.GetNextConnectionId();
const TimeoutId timeoutId = m_connectionTimeoutQueue.RegisterItem(aznumeric_cast<uint64_t>(connectionId), m_timeoutMs);
const AZ::TimeMs timeoutTimeMs = m_timeoutMs / static_cast<AZ::TimeMs>(static_cast<int32_t>(net_UdpUnackedHeartbeats));
const TimeoutId timeoutId = m_connectionTimeoutQueue.RegisterItem(aznumeric_cast<uint64_t>(connectionId), timeoutTimeMs);
AZStd::unique_ptr<UdpConnection> connection = AZStd::make_unique<UdpConnection>(connectionId, remoteAddress, *this, ConnectionRole::Connector);
UdpPacketEncodingBuffer dtlsData;
@@ -277,6 +278,7 @@ namespace AzNetworking
}
timeoutItem->UpdateTimeoutTime(startTimeMs);
connection->m_timeoutCounter = 0;
PacketDispatchResult handledPacket = PacketDispatchResult::Failure;
if (header.GetPacketType() < aznumeric_cast<PacketType>(CorePackets::PacketType::MAX))
@@ -319,16 +321,10 @@ namespace AzNetworking
const AZ::TimeMs receiveTimeMs = AZ::GetElapsedTimeMs() - startTimeMs;
// Time out any stale client connections
{
ConnectionTimeoutFunctor functor(*this);
m_connectionTimeoutQueue.UpdateTimeouts(functor);
}
m_connectionTimeoutQueue.UpdateTimeouts([this](TimeoutQueue::TimeoutItem& item) { return HandleConnectionTimeout(item); });
// Time out any packets that haven't been acked within our timeout window
{
PacketTimeoutFunctor functor(*this);
m_packetTimeoutQueue.UpdateTimeouts(functor, static_cast<int32_t>(net_MaxTimeoutsPerFrame));
}
m_packetTimeoutQueue.UpdateTimeouts([this](TimeoutQueue::TimeoutItem& item) { return HandlePacketTimeout(item); }, static_cast<int32_t>(net_MaxTimeoutsPerFrame));
// Delete any connections we've disconnected
for (RemovedConnection& removedConnection : m_removedConnections)
@@ -709,21 +705,14 @@ namespace AzNetworking
{
// Packets involved in handshake are InitiateConnection, ConnectionHandshake and FragmentedPackets of ConnectionHandshake
return packetType == aznumeric_cast<PacketType>(CorePackets::PacketType::InitiateConnectionPacket) ||
packetType == aznumeric_cast<PacketType>(CorePackets::PacketType::ConnectionHandshakePacket) ||
(packetType == aznumeric_cast<PacketType>(CorePackets::PacketType::FragmentedPacket) && endpoint.IsConnecting());
packetType == aznumeric_cast<PacketType>(CorePackets::PacketType::ConnectionHandshakePacket) ||
(packetType == aznumeric_cast<PacketType>(CorePackets::PacketType::FragmentedPacket) && endpoint.IsConnecting());
}
UdpNetworkInterface::ConnectionTimeoutFunctor::ConnectionTimeoutFunctor(UdpNetworkInterface& networkInterface)
: m_networkInterface(networkInterface)
{
;
}
TimeoutResult UdpNetworkInterface::ConnectionTimeoutFunctor::HandleTimeout(TimeoutQueue::TimeoutItem& item)
TimeoutResult UdpNetworkInterface::HandleConnectionTimeout(TimeoutQueue::TimeoutItem& item)
{
const ConnectionId connectionId = ConnectionId(aznumeric_cast<uint32_t>(item.m_userData));
UdpConnection* udpConnection = static_cast<UdpConnection*>(m_networkInterface.m_connectionSet.GetConnection(connectionId));
UdpConnection* udpConnection = static_cast<UdpConnection*>(m_connectionSet.GetConnection(connectionId));
if (udpConnection == nullptr)
{
@@ -731,22 +720,23 @@ namespace AzNetworking
return TimeoutResult::Delete;
}
if (udpConnection->GetConnectionState() == ConnectionState::Connecting)
if ((udpConnection->GetConnectionState() == ConnectionState::Connecting)
&& udpConnection->GetDtlsEndpoint().IsConnecting())
{
if (udpConnection->GetDtlsEndpoint().IsConnecting())
{
// DTLS prefers we resend data lost over the wire with fresh SSL IDs so account for that here
UdpPacketEncodingBuffer dtlsData;
udpConnection->ProcessHandshakeData(dtlsData);
return TimeoutResult::Refresh;
}
// DTLS prefers we resend data lost over the wire with fresh SSL IDs so account for that here
UdpPacketEncodingBuffer dtlsData;
udpConnection->ProcessHandshakeData(dtlsData);
return TimeoutResult::Refresh;
}
if (udpConnection->GetConnectionRole() == ConnectionRole::Connector)
if ((udpConnection->GetConnectionRole() == ConnectionRole::Connector)
&& (udpConnection->m_timeoutCounter < net_UdpUnackedHeartbeats))
{
udpConnection->SendUnreliablePacket(CorePackets::HeartbeatPacket());
// Set the request response flag to true since we want a response to keep the connection alive
udpConnection->SendUnreliablePacket(CorePackets::HeartbeatPacket(true));
++udpConnection->m_timeoutCounter;
}
else if (net_UdpTimeoutConnections && (m_networkInterface.GetTimeoutMs() > AZ::TimeMs{ 0 }))
else if (net_UdpTimeoutConnections && (GetTimeoutMs() > AZ::TimeMs{ 0 }))
{
udpConnection->Disconnect(DisconnectReason::Timeout, TerminationEndpoint::Local);
return TimeoutResult::Delete;
@@ -755,19 +745,13 @@ namespace AzNetworking
return TimeoutResult::Refresh;
}
UdpNetworkInterface::PacketTimeoutFunctor::PacketTimeoutFunctor(UdpNetworkInterface& networkInterface)
: m_networkInterface(networkInterface)
{
;
}
TimeoutResult UdpNetworkInterface::PacketTimeoutFunctor::HandleTimeout(TimeoutQueue::TimeoutItem& item)
TimeoutResult UdpNetworkInterface::HandlePacketTimeout(TimeoutQueue::TimeoutItem& item)
{
ConnectionId connectionId;
PacketId packetId;
ReliabilityType reliability;
DecodeTimeoutId(item.m_userData, connectionId, packetId, reliability);
UdpConnection* connection = static_cast<UdpConnection*>(m_networkInterface.m_connectionSet.GetConnection(connectionId));
UdpConnection* connection = static_cast<UdpConnection*>(m_connectionSet.GetConnection(connectionId));
if (connection == nullptr)
{
@@ -782,16 +766,14 @@ namespace AzNetworking
case PacketTimeoutResult::Acked:
// Packet was already acked, just discard this timeout entry
return TimeoutResult::Delete;
case PacketTimeoutResult::Pending:
// Packet timed out before we received any info about it's sequence from the remote endpoint
// The connection latency may have increased, and our Rtt metrics may still be adjusting..
// Just throw it back into the timeout queue
return TimeoutResult::Refresh;
case PacketTimeoutResult::Lost:
// Packet timed out and was not acked, so we consider it lost
m_networkInterface.m_connectionListener.OnPacketLost(connection, packetId);
m_connectionListener.OnPacketLost(connection, packetId);
break;
}

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