Merge branch 'development' into LYN-5265_state_tracker_impl

monroegm-disable-blank-issue-2
John 4 years ago
commit 1cdb34326b

@ -0,0 +1,162 @@
#
# 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
#
#
import os, traceback, binascii, sys, json, pathlib
import azlmbr.math
import azlmbr.bus
#
# SceneAPI Processor
#
def log_exception_traceback():
exc_type, exc_value, exc_tb = sys.exc_info()
data = traceback.format_exception(exc_type, exc_value, exc_tb)
print(str(data))
def get_mesh_node_names(sceneGraph):
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
meshDataList = []
node = sceneGraph.get_root()
children = []
paths = []
while node.IsValid():
# store children to process after siblings
if sceneGraph.has_node_child(node):
children.append(sceneGraph.get_node_child(node))
nodeName = sceneData.SceneGraphName(sceneGraph.get_node_name(node))
paths.append(nodeName.get_path())
# store any node that has mesh data content
nodeContent = sceneGraph.get_node_content(node)
if nodeContent.CastWithTypeName('MeshData'):
if sceneGraph.is_node_end_point(node) is False:
if (len(nodeName.get_path())):
meshDataList.append(sceneData.SceneGraphName(sceneGraph.get_node_name(node)))
# advance to next node
if sceneGraph.has_node_sibling(node):
node = sceneGraph.get_node_sibling(node)
elif children:
node = children.pop()
else:
node = azlmbr.scene.graph.NodeIndex()
return meshDataList, paths
def update_manifest(scene):
import json
import uuid, os
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
graph = sceneData.SceneGraph(scene.graph)
# Get a list of all the mesh nodes, as well as all the nodes
mesh_name_list, all_node_paths = get_mesh_node_names(graph)
scene_manifest = sceneData.SceneManifest()
clean_filename = scene.sourceFilename.replace('.', '_')
# Compute the filename of the scene file
source_basepath = scene.watchFolder
source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
source_filename_only = os.path.basename(clean_filename)
created_entities = []
# Loop every mesh node in the scene
for activeMeshIndex in range(len(mesh_name_list)):
mesh_name = mesh_name_list[activeMeshIndex]
mesh_path = mesh_name.get_path()
# Create a unique mesh group name using the filename + node name
mesh_group_name = '{}_{}'.format(source_filename_only, mesh_name.get_name())
# Remove forbidden filename characters from the name since this will become a file on disk later
mesh_group_name = "".join(char for char in mesh_group_name if char not in "|<>:\"/?*\\")
# Add the MeshGroup to the manifest and give it a unique ID
mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
# Set our current node as the only node that is included in this MeshGroup
scene_manifest.mesh_group_select_node(mesh_group, mesh_path)
# Explicitly remove all other nodes to prevent implicit inclusions
for node in all_node_paths:
if node != mesh_path:
scene_manifest.mesh_group_unselect_node(mesh_group, node)
# Create an editor entity
entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
# Add an EditorMeshComponent to the entity
editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "AZ::Render::EditorMeshComponent")
# Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just created + the azmodel extension
# The MeshGroup we created will be output as a product in the asset's path named mesh_group_name.azmodel
# The assetHint will be converted to an AssetId later during prefab loading
json_update = json.dumps({
"Controller": { "Configuration": { "ModelAsset": {
"assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel" }}}
});
# Apply the JSON above to the component we created
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_mesh_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity failed")
# Keep track of the entity we set up, we'll add them all to the prefab we're creating later
created_entities.append(entity_id)
# Create a prefab with all our entities
prefab_filename = source_filename_only + ".prefab"
created_template_id = azlmbr.prefab.PrefabSystemScriptingBus(azlmbr.bus.Broadcast, "CreatePrefab", created_entities, prefab_filename)
if created_template_id == azlmbr.prefab.InvalidTemplateId:
raise RuntimeError("CreatePrefab {} failed".format(prefab_filename))
# Convert the prefab to a JSON string
output = azlmbr.prefab.PrefabLoaderScriptingBus(azlmbr.bus.Broadcast, "SaveTemplateToString", created_template_id)
if output.IsSuccess():
jsonString = output.GetValue()
uuid = azlmbr.math.Uuid_CreateRandom().ToString()
jsonResult = json.loads(jsonString)
# Add a PrefabGroup to the manifest and store the JSON on it
scene_manifest.add_prefab_group(source_filename_only, uuid, jsonResult)
else:
raise RuntimeError("SaveTemplateToString failed for template id {}, prefab {}".format(created_template_id, prefab_filename))
# Convert the manifest to a JSON string and return it
new_manifest = scene_manifest.export()
return new_manifest
sceneJobHandler = None
def on_update_manifest(args):
try:
scene = args[0]
return update_manifest(scene)
except RuntimeError as err:
print (f'ERROR - {err}')
log_exception_traceback()
global sceneJobHandler
sceneJobHandler = None
# try to create SceneAPI handler for processing
try:
import azlmbr.scene as sceneApi
if (sceneJobHandler == None):
sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
sceneJobHandler.connect()
sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
except:
sceneJobHandler = None

@ -21,7 +21,6 @@ set(ENABLED_GEMS
QtForPython
PythonAssetBuilder
Metastream
Camera
EMotionFX
AtomTressFX
@ -53,6 +52,6 @@ set(ENABLED_GEMS
AWSCore
AWSClientAuth
AWSMetrics
PrefabBuilder
AudioSystem
)

@ -6,12 +6,7 @@
#
#
################################################################################
# Atom Renderer: Automated Tests
# Runs EditorPythonBindings (hydra) scripts inside the Editor to verify test results for the Atom renderer.
################################################################################
if(PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_BUILD_TESTS_SUPPORTED AND AutomatedTesting IN_LIST LY_PROJECTS)
if(PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_BUILD_TESTS_SUPPORTED)
ly_add_pytest(
NAME AutomatedTesting::Atom_TestSuite_Main
TEST_SUITE main

@ -25,6 +25,7 @@ TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests")
class TestAtomEditorComponentsMain(object):
"""Holds tests for Atom components."""
@pytest.mark.xfail(reason="This test is being marked xfail as it failed during an unrelated development run. See LYN-7530 for more details.")
def test_AtomEditorComponents_AddedToEntity(self, request, editor, level, workspace, project, launcher_platform):
"""
Please review the hydra script run by this test for more specific test info.

@ -220,20 +220,18 @@ class TestPerformanceBenchmarkSuite(object):
@pytest.mark.system
class TestMaterialEditor(object):
@pytest.mark.parametrize("cfg_args", ["-rhi=dx12", "-rhi=Vulkan"])
@pytest.mark.parametrize("cfg_args,expected_lines", [
pytest.param("-rhi=dx12", ["Registering dx12 RHI"]),
pytest.param("-rhi=Vulkan", ["Registering vulkan RHI"])
])
@pytest.mark.parametrize("exe_file_name", ["MaterialEditor"])
def test_MaterialEditorLaunch_AllRHIOptionsSucceed(
self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name, cfg_args):
self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name, cfg_args,
expected_lines):
"""
Tests each valid RHI option (Null RHI excluded) can be launched with the MaterialEditor.
Checks for the "Finished loading viewport configurations." success message post launch.
Checks for the specific expected_lines messaging for each RHI type.
"""
expected_lines = ["Finished loading viewport configurations."]
unexpected_lines = [
# "Trace::Assert",
# "Trace::Error",
"Traceback (most recent call last):",
]
hydra.launch_and_validate_results(
request,
@ -243,7 +241,7 @@ class TestMaterialEditor(object):
run_python="--runpython",
timeout=60,
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
unexpected_lines=[],
halt_on_unexpected=False,
null_renderer=False,
cfg_args=[cfg_args],

@ -14,33 +14,50 @@ from ly_test_tools.o3de.editor_test import EditorSharedTest, EditorTestSuite
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
class TestAutomation(EditorTestSuite):
@pytest.mark.test_case_id("C32078118")
class AtomEditorComponents_DecalAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DecalAdded as test_module
@pytest.mark.test_case_id("C32078119")
class AtomEditorComponents_DepthOfFieldAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DepthOfFieldAdded as test_module
@pytest.mark.test_case_id("C32078120")
class AtomEditorComponents_DirectionalLightAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DirectionalLightAdded as test_module
@pytest.mark.test_case_id("C32078121")
class AtomEditorComponents_ExposureControlAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_ExposureControlAdded as test_module
@pytest.mark.test_case_id("C32078115")
class AtomEditorComponents_GlobalSkylightIBLAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_GlobalSkylightIBLAdded as test_module
@pytest.mark.test_case_id("C32078125")
class AtomEditorComponents_PhysicalSkyAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_PhysicalSkyAdded as test_module
@pytest.mark.test_case_id("C32078131")
class AtomEditorComponents_PostFXRadiusWeightModifierAdded(EditorSharedTest):
from Atom.tests import (
hydra_AtomEditorComponents_PostFXRadiusWeightModifierAdded as test_module)
@pytest.mark.test_case_id("C32078117")
class AtomEditorComponents_LightAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_LightAdded as test_module
@pytest.mark.test_case_id("C36525660")
class AtomEditorComponents_DisplayMapperAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DisplayMapperAdded as test_module
@pytest.mark.test_case_id("C32078128")
class AtomEditorComponents_ReflectionProbeAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_ReflectionProbeAdded as test_module
@pytest.mark.test_case_id("C32078124")
class AtomEditorComponents_MeshAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_MeshAdded as test_module
class ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(EditorSharedTest):
from Atom.tests import hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges as test_module

@ -0,0 +1,171 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
class Tests:
creation_undo = (
"UNDO Entity creation success",
"UNDO Entity creation failed")
creation_redo = (
"REDO Entity creation success",
"REDO Entity creation failed")
mesh_entity_creation = (
"Mesh Entity successfully created",
"Mesh Entity failed to be created")
mesh_component_added = (
"Entity has a Mesh component",
"Entity failed to find Mesh component")
mesh_asset_specified = (
"Mesh asset set",
"Mesh asset not set")
enter_game_mode = (
"Entered game mode",
"Failed to enter game mode")
exit_game_mode = (
"Exited game mode",
"Couldn't exit game mode")
is_visible = (
"Entity is visible",
"Entity was not visible")
is_hidden = (
"Entity is hidden",
"Entity was not hidden")
entity_deleted = (
"Entity deleted",
"Entity was not deleted")
deletion_undo = (
"UNDO deletion success",
"UNDO deletion failed")
deletion_redo = (
"REDO deletion success",
"REDO deletion failed")
def AtomEditorComponents_Mesh_AddedToEntity():
"""
Summary:
Tests the Mesh component can be added to an entity and has the expected functionality.
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
Expected Behavior:
The component can be added, used in game mode, hidden/shown, deleted, and has accurate required components.
Creation and deletion undo/redo should also work.
Test Steps:
1) Create a Mesh entity with no components.
2) Add a Mesh component to Mesh entity.
3) UNDO the entity creation and component addition.
4) REDO the entity creation and component addition.
5) Specify the Mesh component asset
6) Enter/Exit game mode.
7) Test IsHidden.
8) Test IsVisible.
9) Delete Mesh entity.
10) UNDO deletion.
11) REDO deletion.
12) Look for errors.
:return: None
"""
import os
import azlmbr.legacy.general as general
from editor_python_test_tools.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper as helper
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
helper.init_idle()
helper.open_level("", "Base")
# Test steps begin.
# 1. Create a Mesh entity with no components.
mesh_name = "Mesh"
mesh_entity = EditorEntity.create_editor_entity(mesh_name)
Report.critical_result(Tests.mesh_entity_creation, mesh_entity.exists())
# 2. Add a Mesh component to Mesh entity.
mesh_component = mesh_entity.add_component(mesh_name)
Report.critical_result(
Tests.mesh_component_added,
mesh_entity.has_component(mesh_name))
# 3. UNDO the entity creation and component addition.
# -> UNDO component addition.
general.undo()
# -> UNDO naming entity.
general.undo()
# -> UNDO selecting entity.
general.undo()
# -> UNDO entity creation.
general.undo()
general.idle_wait_frames(1)
Report.result(Tests.creation_undo, not mesh_entity.exists())
# 4. REDO the entity creation and component addition.
# -> REDO entity creation.
general.redo()
# -> REDO selecting entity.
general.redo()
# -> REDO naming entity.
general.redo()
# -> REDO component addition.
general.redo()
general.idle_wait_frames(1)
Report.result(Tests.creation_redo, mesh_entity.exists())
# 5. Set Mesh component asset property
mesh_property_asset = 'Controller|Configuration|Mesh Asset'
model_path = os.path.join('Objects', 'shaderball', 'shaderball_default_1m.azmodel')
model = Asset.find_asset_by_path(model_path)
mesh_component.set_component_property_value(mesh_property_asset, model.id)
Report.result(Tests.mesh_asset_specified,
mesh_component.get_component_property_value(mesh_property_asset) == model.id)
# 6. Enter/Exit game mode.
helper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
helper.exit_game_mode(Tests.exit_game_mode)
# 7. Test IsHidden.
mesh_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, mesh_entity.is_hidden() is True)
# 8. Test IsVisible.
mesh_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, mesh_entity.is_visible() is True)
# 9. Delete Mesh entity.
mesh_entity.delete()
Report.result(Tests.entity_deleted, not mesh_entity.exists())
# 10. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, mesh_entity.exists())
# 11. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not mesh_entity.exists())
# 12. Look for errors or asserts.
helper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponents_Mesh_AddedToEntity)

@ -0,0 +1,194 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
class Tests:
creation_undo = (
"UNDO Entity creation success",
"UNDO Entity creation failed")
creation_redo = (
"REDO Entity creation success",
"REDO Entity creation failed")
reflection_probe_creation = (
"Reflection Probe Entity successfully created",
"Reflection Probe Entity failed to be created")
reflection_probe_component = (
"Entity has a Reflection Probe component",
"Entity failed to find Reflection Probe component")
reflection_probe_disabled = (
"Reflection Probe component disabled",
"Reflection Probe component was not disabled.")
reflection_map_generated = (
"Reflection Probe cubemap generated",
"Reflection Probe cubemap not generated")
box_shape_component = (
"Entity has a Box Shape component",
"Entity did not have a Box Shape component")
reflection_probe_enabled = (
"Reflection Probe component enabled",
"Reflection Probe component was not enabled.")
enter_game_mode = (
"Entered game mode",
"Failed to enter game mode")
exit_game_mode = (
"Exited game mode",
"Couldn't exit game mode")
is_visible = (
"Entity is visible",
"Entity was not visible")
is_hidden = (
"Entity is hidden",
"Entity was not hidden")
entity_deleted = (
"Entity deleted",
"Entity was not deleted")
deletion_undo = (
"UNDO deletion success",
"UNDO deletion failed")
deletion_redo = (
"REDO deletion success",
"REDO deletion failed")
def AtomEditorComponents_ReflectionProbe_AddedToEntity():
"""
Summary:
Tests the Reflection Probe component can be added to an entity and has the expected functionality.
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
Expected Behavior:
The component can be added, used in game mode, hidden/shown, deleted, and has accurate required components.
Creation and deletion undo/redo should also work.
Test Steps:
1) Create a Reflection Probe entity with no components.
2) Add a Reflection Probe component to Reflection Probe entity.
3) UNDO the entity creation and component addition.
4) REDO the entity creation and component addition.
5) Verify Reflection Probe component not enabled.
6) Add Shape component since it is required by the Reflection Probe component.
7) Verify Reflection Probe component is enabled.
8) Enter/Exit game mode.
9) Test IsHidden.
10) Test IsVisible.
11) Verify cubemap generation
12) Delete Reflection Probe entity.
13) UNDO deletion.
14) REDO deletion.
15) Look for errors.
:return: None
"""
import azlmbr.legacy.general as general
import azlmbr.math as math
import azlmbr.render as render
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper as helper
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
helper.init_idle()
helper.open_level("", "Base")
# Test steps begin.
# 1. Create a Reflection Probe entity with no components.
reflection_probe_name = "Reflection Probe"
reflection_probe_entity = EditorEntity.create_editor_entity_at(
math.Vector3(512.0, 512.0, 34.0), reflection_probe_name)
Report.critical_result(Tests.reflection_probe_creation, reflection_probe_entity.exists())
# 2. Add a Reflection Probe component to Reflection Probe entity.
reflection_probe_component = reflection_probe_entity.add_component(reflection_probe_name)
Report.critical_result(
Tests.reflection_probe_component,
reflection_probe_entity.has_component(reflection_probe_name))
# 3. UNDO the entity creation and component addition.
# -> UNDO component addition.
general.undo()
# -> UNDO naming entity.
general.undo()
# -> UNDO selecting entity.
general.undo()
# -> UNDO entity creation.
general.undo()
general.idle_wait_frames(1)
Report.result(Tests.creation_undo, not reflection_probe_entity.exists())
# 4. REDO the entity creation and component addition.
# -> REDO entity creation.
general.redo()
# -> REDO selecting entity.
general.redo()
# -> REDO naming entity.
general.redo()
# -> REDO component addition.
general.redo()
general.idle_wait_frames(1)
Report.result(Tests.creation_redo, reflection_probe_entity.exists())
# 5. Verify Reflection Probe component not enabled.
Report.result(Tests.reflection_probe_disabled, not reflection_probe_component.is_enabled())
# 6. Add Box Shape component since it is required by the Reflection Probe component.
box_shape = "Box Shape"
reflection_probe_entity.add_component(box_shape)
Report.result(Tests.box_shape_component, reflection_probe_entity.has_component(box_shape))
# 7. Verify Reflection Probe component is enabled.
Report.result(Tests.reflection_probe_enabled, reflection_probe_component.is_enabled())
# 8. Enter/Exit game mode.
helper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
helper.exit_game_mode(Tests.exit_game_mode)
# 9. Test IsHidden.
reflection_probe_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, reflection_probe_entity.is_hidden() is True)
# 10. Test IsVisible.
reflection_probe_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, reflection_probe_entity.is_visible() is True)
# 11. Verify cubemap generation
render.EditorReflectionProbeBus(azlmbr.bus.Event, "BakeReflectionProbe", reflection_probe_entity.id)
Report.result(
Tests.reflection_map_generated,
helper.wait_for_condition(
lambda: reflection_probe_component.get_component_property_value("Cubemap|Baked Cubemap Path") != "",
20.0))
# 12. Delete Reflection Probe entity.
reflection_probe_entity.delete()
Report.result(Tests.entity_deleted, not reflection_probe_entity.exists())
# 13. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, reflection_probe_entity.exists())
# 14. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not reflection_probe_entity.exists())
# 15. Look for errors or asserts.
helper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponents_ReflectionProbe_AddedToEntity)

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:827a63985273229050bf4f63030bcc666f045091fe81cf8157a9ca23b40074b6
size 3214

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

@ -0,0 +1,8 @@
{
"values": [
{
"$type": "ScriptProcessorRule",
"scriptFilename": "Editor/Scripts/scene_mesh_to_prefab.py"
}
]
}

@ -487,8 +487,6 @@ void LevelEditorMenuHandler::PopulateEditMenu(ActionManager::MenuWrapper& editMe
editMenu.AddAction(AzToolsFramework::EditPivot);
editMenu.AddAction(AzToolsFramework::EditReset);
editMenu.AddAction(AzToolsFramework::EditResetManipulator);
editMenu.AddAction(AzToolsFramework::EditResetLocal);
editMenu.AddAction(AzToolsFramework::EditResetWorld);
// Hide Selection
editMenu.AddAction(AzToolsFramework::HideSelection);

@ -2124,6 +2124,8 @@ bool CCryEditApp::FixDanglingSharedMemory(const QString& sharedMemName) const
int CCryEditApp::ExitInstance(int exitCode)
{
AZ_TracePrintf("Exit", "Called ExitInstance() with exit code: 0x%x", exitCode);
if (m_pEditor)
{
m_pEditor->OnBeginShutdownSequence();

@ -95,7 +95,8 @@ namespace SandboxEditor
cameras.AddCamera(m_firstPersonPanCamera);
cameras.AddCamera(m_firstPersonTranslateCamera);
cameras.AddCamera(m_firstPersonScrollCamera);
cameras.AddCamera(m_pivotCamera);
cameras.AddCamera(m_firstPersonFocusCamera);
cameras.AddCamera(m_orbitCamera);
});
return controller;
@ -111,6 +112,7 @@ namespace SandboxEditor
viewportId, &AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Events::BeginCursorCapture);
}
};
const auto showCursor = [viewportId = m_viewportId]
{
if (SandboxEditor::CameraCaptureCursorForLook())
@ -133,7 +135,7 @@ namespace SandboxEditor
m_firstPersonRotateCamera->SetActivationEndedFn(showCursor);
m_firstPersonPanCamera = AZStd::make_shared<AzFramework::PanCameraInput>(
SandboxEditor::CameraFreePanChannelId(), AzFramework::LookPan, AzFramework::TranslatePivot);
SandboxEditor::CameraFreePanChannelId(), AzFramework::LookPan, AzFramework::TranslatePivotLook);
m_firstPersonPanCamera->m_panSpeedFn = []
{
@ -153,7 +155,7 @@ namespace SandboxEditor
const auto translateCameraInputChannelIds = BuildTranslateCameraInputChannelIds();
m_firstPersonTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslatePivot);
translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslatePivotLook);
m_firstPersonTranslateCamera->m_translateSpeedFn = []
{
@ -165,90 +167,111 @@ namespace SandboxEditor
return SandboxEditor::CameraBoostMultiplier();
};
m_firstPersonScrollCamera = AZStd::make_shared<AzFramework::ScrollTranslationCameraInput>();
m_firstPersonScrollCamera = AZStd::make_shared<AzFramework::LookScrollTranslationCameraInput>();
m_firstPersonScrollCamera->m_scrollSpeedFn = []
{
return SandboxEditor::CameraScrollSpeed();
};
m_pivotCamera = AZStd::make_shared<AzFramework::PivotCameraInput>(SandboxEditor::CameraPivotChannelId());
const auto pivotFn = []
{
// use the manipulator transform as the pivot point
AZStd::optional<AZ::Transform> entityPivot;
AzToolsFramework::EditorTransformComponentSelectionRequestBus::EventResult(
entityPivot, AzToolsFramework::GetEntityContextId(),
&AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::GetManipulatorTransform);
m_pivotCamera->SetPivotFn(
[]([[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
if (entityPivot.has_value())
{
// use the manipulator transform as the pivot point
AZStd::optional<AZ::Transform> entityPivot;
AzToolsFramework::EditorTransformComponentSelectionRequestBus::EventResult(
entityPivot, AzToolsFramework::GetEntityContextId(),
&AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::GetManipulatorTransform);
// otherwise just use the identity
return entityPivot.value_or(AZ::Transform::CreateIdentity()).GetTranslation();
return entityPivot->GetTranslation();
}
// otherwise just use the identity
return AZ::Vector3::CreateZero();
};
m_firstPersonFocusCamera =
AZStd::make_shared<AzFramework::FocusCameraInput>(SandboxEditor::CameraFocusChannelId(), AzFramework::FocusLook);
m_firstPersonFocusCamera->SetPivotFn(pivotFn);
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)
{
return pivotFn();
});
m_pivotRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraPivotLookChannelId());
m_orbitRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraOrbitLookChannelId());
m_pivotRotateCamera->m_rotateSpeedFn = []
m_orbitRotateCamera->m_rotateSpeedFn = []
{
return SandboxEditor::CameraRotateSpeed();
};
m_pivotRotateCamera->m_invertYawFn = []
m_orbitRotateCamera->m_invertYawFn = []
{
return SandboxEditor::CameraPivotYawRotationInverted();
return SandboxEditor::CameraOrbitYawRotationInverted();
};
m_pivotTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslateOffset);
m_orbitTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslateOffsetOrbit);
m_pivotTranslateCamera->m_translateSpeedFn = []
m_orbitTranslateCamera->m_translateSpeedFn = []
{
return SandboxEditor::CameraTranslateSpeed();
};
m_pivotTranslateCamera->m_boostMultiplierFn = []
m_orbitTranslateCamera->m_boostMultiplierFn = []
{
return SandboxEditor::CameraBoostMultiplier();
};
m_pivotDollyScrollCamera = AZStd::make_shared<AzFramework::PivotDollyScrollCameraInput>();
m_orbitDollyScrollCamera = AZStd::make_shared<AzFramework::OrbitDollyScrollCameraInput>();
m_pivotDollyScrollCamera->m_scrollSpeedFn = []
m_orbitDollyScrollCamera->m_scrollSpeedFn = []
{
return SandboxEditor::CameraScrollSpeed();
};
m_pivotDollyMoveCamera = AZStd::make_shared<AzFramework::PivotDollyMotionCameraInput>(SandboxEditor::CameraPivotDollyChannelId());
m_orbitDollyMoveCamera = AZStd::make_shared<AzFramework::OrbitDollyMotionCameraInput>(SandboxEditor::CameraOrbitDollyChannelId());
m_pivotDollyMoveCamera->m_motionSpeedFn = []
m_orbitDollyMoveCamera->m_motionSpeedFn = []
{
return SandboxEditor::CameraDollyMotionSpeed();
};
m_pivotPanCamera = AZStd::make_shared<AzFramework::PanCameraInput>(
SandboxEditor::CameraPivotPanChannelId(), AzFramework::LookPan, AzFramework::TranslateOffset);
m_orbitPanCamera = AZStd::make_shared<AzFramework::PanCameraInput>(
SandboxEditor::CameraOrbitPanChannelId(), AzFramework::LookPan, AzFramework::TranslateOffsetOrbit);
m_pivotPanCamera->m_panSpeedFn = []
m_orbitPanCamera->m_panSpeedFn = []
{
return SandboxEditor::CameraPanSpeed();
};
m_pivotPanCamera->m_invertPanXFn = []
m_orbitPanCamera->m_invertPanXFn = []
{
return SandboxEditor::CameraPanInvertedX();
};
m_pivotPanCamera->m_invertPanYFn = []
m_orbitPanCamera->m_invertPanYFn = []
{
return SandboxEditor::CameraPanInvertedY();
};
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotRotateCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotTranslateCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotDollyScrollCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotDollyMoveCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotPanCamera);
m_orbitFocusCamera =
AZStd::make_shared<AzFramework::FocusCameraInput>(SandboxEditor::CameraFocusChannelId(), AzFramework::FocusOrbit);
m_orbitFocusCamera->SetPivotFn(pivotFn);
m_orbitCamera->m_orbitCameras.AddCamera(m_orbitRotateCamera);
m_orbitCamera->m_orbitCameras.AddCamera(m_orbitTranslateCamera);
m_orbitCamera->m_orbitCameras.AddCamera(m_orbitDollyScrollCamera);
m_orbitCamera->m_orbitCameras.AddCamera(m_orbitDollyMoveCamera);
m_orbitCamera->m_orbitCameras.AddCamera(m_orbitPanCamera);
m_orbitCamera->m_orbitCameras.AddCamera(m_orbitFocusCamera);
}
void EditorModularViewportCameraComposer::OnEditorModularViewportCameraComposerSettingsChanged()
@ -257,12 +280,14 @@ namespace SandboxEditor
m_firstPersonTranslateCamera->SetTranslateCameraInputChannelIds(translateCameraInputChannelIds);
m_firstPersonPanCamera->SetPanInputChannelId(SandboxEditor::CameraFreePanChannelId());
m_firstPersonRotateCamera->SetRotateInputChannelId(SandboxEditor::CameraFreeLookChannelId());
m_pivotCamera->SetPivotInputChannelId(SandboxEditor::CameraPivotChannelId());
m_pivotTranslateCamera->SetTranslateCameraInputChannelIds(translateCameraInputChannelIds);
m_pivotPanCamera->SetPanInputChannelId(SandboxEditor::CameraPivotPanChannelId());
m_pivotRotateCamera->SetRotateInputChannelId(SandboxEditor::CameraPivotLookChannelId());
m_pivotDollyMoveCamera->SetDollyInputChannelId(SandboxEditor::CameraPivotDollyChannelId());
m_firstPersonFocusCamera->SetFocusInputChannelId(SandboxEditor::CameraFocusChannelId());
m_orbitCamera->SetOrbitInputChannelId(SandboxEditor::CameraOrbitChannelId());
m_orbitTranslateCamera->SetTranslateCameraInputChannelIds(translateCameraInputChannelIds);
m_orbitPanCamera->SetPanInputChannelId(SandboxEditor::CameraOrbitPanChannelId());
m_orbitRotateCamera->SetRotateInputChannelId(SandboxEditor::CameraOrbitLookChannelId());
m_orbitDollyMoveCamera->SetDollyInputChannelId(SandboxEditor::CameraOrbitDollyChannelId());
m_orbitFocusCamera->SetFocusInputChannelId(SandboxEditor::CameraFocusChannelId());
}
void EditorModularViewportCameraComposer::OnViewportViewEntityChanged(const AZ::EntityId& viewEntityId)

@ -41,13 +41,15 @@ namespace SandboxEditor
AZStd::shared_ptr<AzFramework::RotateCameraInput> m_firstPersonRotateCamera;
AZStd::shared_ptr<AzFramework::PanCameraInput> m_firstPersonPanCamera;
AZStd::shared_ptr<AzFramework::TranslateCameraInput> m_firstPersonTranslateCamera;
AZStd::shared_ptr<AzFramework::ScrollTranslationCameraInput> m_firstPersonScrollCamera;
AZStd::shared_ptr<AzFramework::PivotCameraInput> m_pivotCamera;
AZStd::shared_ptr<AzFramework::RotateCameraInput> m_pivotRotateCamera;
AZStd::shared_ptr<AzFramework::TranslateCameraInput> m_pivotTranslateCamera;
AZStd::shared_ptr<AzFramework::PivotDollyScrollCameraInput> m_pivotDollyScrollCamera;
AZStd::shared_ptr<AzFramework::PivotDollyMotionCameraInput> m_pivotDollyMoveCamera;
AZStd::shared_ptr<AzFramework::PanCameraInput> m_pivotPanCamera;
AZStd::shared_ptr<AzFramework::LookScrollTranslationCameraInput> m_firstPersonScrollCamera;
AZStd::shared_ptr<AzFramework::FocusCameraInput> m_firstPersonFocusCamera;
AZStd::shared_ptr<AzFramework::OrbitCameraInput> m_orbitCamera;
AZStd::shared_ptr<AzFramework::RotateCameraInput> m_orbitRotateCamera;
AZStd::shared_ptr<AzFramework::TranslateCameraInput> m_orbitTranslateCamera;
AZStd::shared_ptr<AzFramework::OrbitDollyScrollCameraInput> m_orbitDollyScrollCamera;
AZStd::shared_ptr<AzFramework::OrbitDollyMotionCameraInput> m_orbitDollyMoveCamera;
AZStd::shared_ptr<AzFramework::PanCameraInput> m_orbitPanCamera;
AZStd::shared_ptr<AzFramework::FocusCameraInput> m_orbitFocusCamera;
AzFramework::ViewportId m_viewportId;
};

@ -73,7 +73,7 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Field("TranslateSmoothing", &CameraMovementSettings::m_translateSmoothing)
->Field("TranslateSmoothness", &CameraMovementSettings::m_translateSmoothness)
->Field("CaptureCursorLook", &CameraMovementSettings::m_captureCursorLook)
->Field("PivotYawRotationInverted", &CameraMovementSettings::m_pivotYawRotationInverted)
->Field("OrbitYawRotationInverted", &CameraMovementSettings::m_orbitYawRotationInverted)
->Field("PanInvertedX", &CameraMovementSettings::m_panInvertedX)
->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY);
@ -86,12 +86,13 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Field("TranslateUp", &CameraInputSettings::m_translateUpChannelId)
->Field("TranslateDown", &CameraInputSettings::m_translateDownChannelId)
->Field("Boost", &CameraInputSettings::m_boostChannelId)
->Field("Pivot", &CameraInputSettings::m_pivotChannelId)
->Field("Orbit", &CameraInputSettings::m_orbitChannelId)
->Field("FreeLook", &CameraInputSettings::m_freeLookChannelId)
->Field("FreePan", &CameraInputSettings::m_freePanChannelId)
->Field("PivotLook", &CameraInputSettings::m_pivotLookChannelId)
->Field("PivotDolly", &CameraInputSettings::m_pivotDollyChannelId)
->Field("PivotPan", &CameraInputSettings::m_pivotPanChannelId);
->Field("OrbitLook", &CameraInputSettings::m_orbitLookChannelId)
->Field("OrbitDolly", &CameraInputSettings::m_orbitDollyChannelId)
->Field("OrbitPan", &CameraInputSettings::m_orbitPanChannelId)
->Field("Focus", &CameraInputSettings::m_focusChannelId);
serialize.Class<CEditorPreferencesPage_ViewportCamera>()
->Version(1)
@ -143,8 +144,8 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Attribute(AZ::Edit::Attributes::Min, minValue)
->Attribute(AZ::Edit::Attributes::Visibility, &CameraMovementSettings::TranslateSmoothingVisibility)
->DataElement(
AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_pivotYawRotationInverted, "Camera Pivot Yaw Inverted",
"Inverted yaw rotation while pivoting")
AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_orbitYawRotationInverted, "Camera Orbit Yaw Inverted",
"Inverted yaw rotation while orbiting")
->DataElement(
AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_panInvertedX, "Invert Pan X",
"Invert direction of pan in local X axis")
@ -185,8 +186,8 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
"Key/button to move the camera more quickly")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames)
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_pivotChannelId, "Pivot",
"Key/button to begin the camera pivot behavior")
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_orbitChannelId, "Orbit",
"Key/button to begin the camera orbit behavior")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames)
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_freeLookChannelId, "Free Look",
@ -196,24 +197,27 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_freePanChannelId, "Free Pan", "Key/button to begin camera free pan")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames)
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_pivotLookChannelId, "Pivot Look",
"Key/button to begin camera pivot look")
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_orbitLookChannelId, "Orbit Look",
"Key/button to begin camera orbit look")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames)
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_pivotDollyChannelId, "Pivot Dolly",
"Key/button to begin camera pivot dolly")
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_orbitDollyChannelId, "Orbit Dolly",
"Key/button to begin camera orbit dolly")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames)
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_pivotPanChannelId, "Pivot Pan",
"Key/button to begin camera pivot pan")
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_orbitPanChannelId, "Orbit Pan",
"Key/button to begin camera orbit pan")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames)
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_focusChannelId, "Focus", "Key/button to focus camera orbit")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames);
editContext->Class<CEditorPreferencesPage_ViewportCamera>("Viewport Preferences", "Viewport Preferences")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ_CRC("PropertyVisibility_ShowChildrenOnly", 0xef428f20))
->DataElement(
AZ::Edit::UIHandlers::Default, &CEditorPreferencesPage_ViewportCamera::m_cameraMovementSettings,
"Camera Movement Settings", "Camera Movement Settings")
AZ::Edit::UIHandlers::Default, &CEditorPreferencesPage_ViewportCamera::m_cameraMovementSettings, "Camera Movement Settings",
"Camera Movement Settings")
->DataElement(
AZ::Edit::UIHandlers::Default, &CEditorPreferencesPage_ViewportCamera::m_cameraInputSettings, "Camera Input Settings",
"Camera Input Settings");
@ -264,7 +268,7 @@ void CEditorPreferencesPage_ViewportCamera::OnApply()
SandboxEditor::SetCameraTranslateSmoothness(m_cameraMovementSettings.m_translateSmoothness);
SandboxEditor::SetCameraTranslateSmoothingEnabled(m_cameraMovementSettings.m_translateSmoothing);
SandboxEditor::SetCameraCaptureCursorForLook(m_cameraMovementSettings.m_captureCursorLook);
SandboxEditor::SetCameraPivotYawRotationInverted(m_cameraMovementSettings.m_pivotYawRotationInverted);
SandboxEditor::SetCameraOrbitYawRotationInverted(m_cameraMovementSettings.m_orbitYawRotationInverted);
SandboxEditor::SetCameraPanInvertedX(m_cameraMovementSettings.m_panInvertedX);
SandboxEditor::SetCameraPanInvertedY(m_cameraMovementSettings.m_panInvertedY);
@ -275,12 +279,13 @@ void CEditorPreferencesPage_ViewportCamera::OnApply()
SandboxEditor::SetCameraTranslateUpChannelId(m_cameraInputSettings.m_translateUpChannelId);
SandboxEditor::SetCameraTranslateDownChannelId(m_cameraInputSettings.m_translateDownChannelId);
SandboxEditor::SetCameraTranslateBoostChannelId(m_cameraInputSettings.m_boostChannelId);
SandboxEditor::SetCameraPivotChannelId(m_cameraInputSettings.m_pivotChannelId);
SandboxEditor::SetCameraOrbitChannelId(m_cameraInputSettings.m_orbitChannelId);
SandboxEditor::SetCameraFreeLookChannelId(m_cameraInputSettings.m_freeLookChannelId);
SandboxEditor::SetCameraFreePanChannelId(m_cameraInputSettings.m_freePanChannelId);
SandboxEditor::SetCameraPivotLookChannelId(m_cameraInputSettings.m_pivotLookChannelId);
SandboxEditor::SetCameraPivotDollyChannelId(m_cameraInputSettings.m_pivotDollyChannelId);
SandboxEditor::SetCameraPivotPanChannelId(m_cameraInputSettings.m_pivotPanChannelId);
SandboxEditor::SetCameraOrbitLookChannelId(m_cameraInputSettings.m_orbitLookChannelId);
SandboxEditor::SetCameraOrbitDollyChannelId(m_cameraInputSettings.m_orbitDollyChannelId);
SandboxEditor::SetCameraOrbitPanChannelId(m_cameraInputSettings.m_orbitPanChannelId);
SandboxEditor::SetCameraFocusChannelId(m_cameraInputSettings.m_focusChannelId);
SandboxEditor::EditorModularViewportCameraComposerNotificationBus::Broadcast(
&SandboxEditor::EditorModularViewportCameraComposerNotificationBus::Events::OnEditorModularViewportCameraComposerSettingsChanged);
@ -299,7 +304,7 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings()
m_cameraMovementSettings.m_translateSmoothness = SandboxEditor::CameraTranslateSmoothness();
m_cameraMovementSettings.m_translateSmoothing = SandboxEditor::CameraTranslateSmoothingEnabled();
m_cameraMovementSettings.m_captureCursorLook = SandboxEditor::CameraCaptureCursorForLook();
m_cameraMovementSettings.m_pivotYawRotationInverted = SandboxEditor::CameraPivotYawRotationInverted();
m_cameraMovementSettings.m_orbitYawRotationInverted = SandboxEditor::CameraOrbitYawRotationInverted();
m_cameraMovementSettings.m_panInvertedX = SandboxEditor::CameraPanInvertedX();
m_cameraMovementSettings.m_panInvertedY = SandboxEditor::CameraPanInvertedY();
@ -310,10 +315,11 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings()
m_cameraInputSettings.m_translateUpChannelId = SandboxEditor::CameraTranslateUpChannelId().GetName();
m_cameraInputSettings.m_translateDownChannelId = SandboxEditor::CameraTranslateDownChannelId().GetName();
m_cameraInputSettings.m_boostChannelId = SandboxEditor::CameraTranslateBoostChannelId().GetName();
m_cameraInputSettings.m_pivotChannelId = SandboxEditor::CameraPivotChannelId().GetName();
m_cameraInputSettings.m_orbitChannelId = SandboxEditor::CameraOrbitChannelId().GetName();
m_cameraInputSettings.m_freeLookChannelId = SandboxEditor::CameraFreeLookChannelId().GetName();
m_cameraInputSettings.m_freePanChannelId = SandboxEditor::CameraFreePanChannelId().GetName();
m_cameraInputSettings.m_pivotLookChannelId = SandboxEditor::CameraPivotLookChannelId().GetName();
m_cameraInputSettings.m_pivotDollyChannelId = SandboxEditor::CameraPivotDollyChannelId().GetName();
m_cameraInputSettings.m_pivotPanChannelId = SandboxEditor::CameraPivotPanChannelId().GetName();
m_cameraInputSettings.m_orbitLookChannelId = SandboxEditor::CameraOrbitLookChannelId().GetName();
m_cameraInputSettings.m_orbitDollyChannelId = SandboxEditor::CameraOrbitDollyChannelId().GetName();
m_cameraInputSettings.m_orbitPanChannelId = SandboxEditor::CameraOrbitPanChannelId().GetName();
m_cameraInputSettings.m_focusChannelId = SandboxEditor::CameraFocusChannelId().GetName();
}

@ -54,7 +54,7 @@ private:
float m_translateSmoothness;
bool m_translateSmoothing;
bool m_captureCursorLook;
bool m_pivotYawRotationInverted;
bool m_orbitYawRotationInverted;
bool m_panInvertedX;
bool m_panInvertedY;
@ -80,12 +80,13 @@ private:
AZStd::string m_translateUpChannelId;
AZStd::string m_translateDownChannelId;
AZStd::string m_boostChannelId;
AZStd::string m_pivotChannelId;
AZStd::string m_orbitChannelId;
AZStd::string m_freeLookChannelId;
AZStd::string m_freePanChannelId;
AZStd::string m_pivotLookChannelId;
AZStd::string m_pivotDollyChannelId;
AZStd::string m_pivotPanChannelId;
AZStd::string m_orbitLookChannelId;
AZStd::string m_orbitDollyChannelId;
AZStd::string m_orbitPanChannelId;
AZStd::string m_focusChannelId;
};
CameraMovementSettings m_cameraMovementSettings;

@ -28,7 +28,7 @@ namespace SandboxEditor
constexpr AZStd::string_view CameraRotateSpeedSetting = "/Amazon/Preferences/Editor/Camera/RotateSpeed";
constexpr AZStd::string_view CameraScrollSpeedSetting = "/Amazon/Preferences/Editor/Camera/DollyScrollSpeed";
constexpr AZStd::string_view CameraDollyMotionSpeedSetting = "/Amazon/Preferences/Editor/Camera/DollyMotionSpeed";
constexpr AZStd::string_view CameraPivotYawRotationInvertedSetting = "/Amazon/Preferences/Editor/Camera/YawRotationInverted";
constexpr AZStd::string_view CameraOrbitYawRotationInvertedSetting = "/Amazon/Preferences/Editor/Camera/YawRotationInverted";
constexpr AZStd::string_view CameraPanInvertedXSetting = "/Amazon/Preferences/Editor/Camera/PanInvertedX";
constexpr AZStd::string_view CameraPanInvertedYSetting = "/Amazon/Preferences/Editor/Camera/PanInvertedY";
constexpr AZStd::string_view CameraPanSpeedSetting = "/Amazon/Preferences/Editor/Camera/PanSpeed";
@ -44,12 +44,13 @@ namespace SandboxEditor
constexpr AZStd::string_view CameraTranslateUpIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateUpId";
constexpr AZStd::string_view CameraTranslateDownIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateUpDownId";
constexpr AZStd::string_view CameraTranslateBoostIdSetting = "/Amazon/Preferences/Editor/Camera/TranslateBoostId";
constexpr AZStd::string_view CameraPivotIdSetting = "/Amazon/Preferences/Editor/Camera/PivotId";
constexpr AZStd::string_view CameraOrbitIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitId";
constexpr AZStd::string_view CameraFreeLookIdSetting = "/Amazon/Preferences/Editor/Camera/FreeLookId";
constexpr AZStd::string_view CameraFreePanIdSetting = "/Amazon/Preferences/Editor/Camera/FreePanId";
constexpr AZStd::string_view CameraPivotLookIdSetting = "/Amazon/Preferences/Editor/Camera/PivotLookId";
constexpr AZStd::string_view CameraPivotDollyIdSetting = "/Amazon/Preferences/Editor/Camera/PivotDollyId";
constexpr AZStd::string_view CameraPivotPanIdSetting = "/Amazon/Preferences/Editor/Camera/PivotPanId";
constexpr AZStd::string_view CameraOrbitLookIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitLookId";
constexpr AZStd::string_view CameraOrbitDollyIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitDollyId";
constexpr AZStd::string_view CameraOrbitPanIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitPanId";
constexpr AZStd::string_view CameraFocusIdSetting = "/Amazon/Preferences/Editor/Camera/FocusId";
template<typename T>
void SetRegistry(const AZStd::string_view setting, T&& value)
@ -239,14 +240,14 @@ namespace SandboxEditor
SetRegistry(CameraDollyMotionSpeedSetting, speed);
}
bool CameraPivotYawRotationInverted()
bool CameraOrbitYawRotationInverted()
{
return GetRegistry(CameraPivotYawRotationInvertedSetting, false);
return GetRegistry(CameraOrbitYawRotationInvertedSetting, false);
}
void SetCameraPivotYawRotationInverted(const bool inverted)
void SetCameraOrbitYawRotationInverted(const bool inverted)
{
SetRegistry(CameraPivotYawRotationInvertedSetting, inverted);
SetRegistry(CameraOrbitYawRotationInvertedSetting, inverted);
}
bool CameraPanInvertedX()
@ -403,14 +404,14 @@ namespace SandboxEditor
SetRegistry(CameraTranslateBoostIdSetting, cameraTranslateBoostId);
}
AzFramework::InputChannelId CameraPivotChannelId()
AzFramework::InputChannelId CameraOrbitChannelId()
{
return AzFramework::InputChannelId(GetRegistry(CameraPivotIdSetting, AZStd::string("keyboard_key_modifier_alt_l")).c_str());
return AzFramework::InputChannelId(GetRegistry(CameraOrbitIdSetting, AZStd::string("keyboard_key_modifier_alt_l")).c_str());
}
void SetCameraPivotChannelId(AZStd::string_view cameraPivotId)
void SetCameraOrbitChannelId(AZStd::string_view cameraOrbitId)
{
SetRegistry(CameraPivotIdSetting, cameraPivotId);
SetRegistry(CameraOrbitIdSetting, cameraOrbitId);
}
AzFramework::InputChannelId CameraFreeLookChannelId()
@ -433,33 +434,43 @@ namespace SandboxEditor
SetRegistry(CameraFreePanIdSetting, cameraFreePanId);
}
AzFramework::InputChannelId CameraPivotLookChannelId()
AzFramework::InputChannelId CameraOrbitLookChannelId()
{
return AzFramework::InputChannelId(GetRegistry(CameraPivotLookIdSetting, AZStd::string("mouse_button_left")).c_str());
return AzFramework::InputChannelId(GetRegistry(CameraOrbitLookIdSetting, AZStd::string("mouse_button_left")).c_str());
}
void SetCameraPivotLookChannelId(AZStd::string_view cameraPivotLookId)
void SetCameraOrbitLookChannelId(AZStd::string_view cameraOrbitLookId)
{
SetRegistry(CameraPivotLookIdSetting, cameraPivotLookId);
SetRegistry(CameraOrbitLookIdSetting, cameraOrbitLookId);
}
AzFramework::InputChannelId CameraPivotDollyChannelId()
AzFramework::InputChannelId CameraOrbitDollyChannelId()
{
return AzFramework::InputChannelId(GetRegistry(CameraPivotDollyIdSetting, AZStd::string("mouse_button_right")).c_str());
return AzFramework::InputChannelId(GetRegistry(CameraOrbitDollyIdSetting, AZStd::string("mouse_button_right")).c_str());
}
void SetCameraPivotDollyChannelId(AZStd::string_view cameraPivotDollyId)
void SetCameraOrbitDollyChannelId(AZStd::string_view cameraOrbitDollyId)
{
SetRegistry(CameraPivotDollyIdSetting, cameraPivotDollyId);
SetRegistry(CameraOrbitDollyIdSetting, cameraOrbitDollyId);
}
AzFramework::InputChannelId CameraPivotPanChannelId()
AzFramework::InputChannelId CameraOrbitPanChannelId()
{
return AzFramework::InputChannelId(GetRegistry(CameraPivotPanIdSetting, AZStd::string("mouse_button_middle")).c_str());
return AzFramework::InputChannelId(GetRegistry(CameraOrbitPanIdSetting, AZStd::string("mouse_button_middle")).c_str());
}
void SetCameraPivotPanChannelId(AZStd::string_view cameraPivotPanId)
void SetCameraOrbitPanChannelId(AZStd::string_view cameraOrbitPanId)
{
SetRegistry(CameraPivotPanIdSetting, cameraPivotPanId);
SetRegistry(CameraOrbitPanIdSetting, cameraOrbitPanId);
}
AzFramework::InputChannelId CameraFocusChannelId()
{
return AzFramework::InputChannelId(GetRegistry(CameraFocusIdSetting, AZStd::string("keyboard_key_alphanumeric_X")).c_str());
}
void SetCameraFocusChannelId(AZStd::string_view cameraFocusId)
{
SetRegistry(CameraFocusIdSetting, cameraFocusId);
}
} // namespace SandboxEditor

@ -71,8 +71,8 @@ namespace SandboxEditor
SANDBOX_API float CameraDollyMotionSpeed();
SANDBOX_API void SetCameraDollyMotionSpeed(float speed);
SANDBOX_API bool CameraPivotYawRotationInverted();
SANDBOX_API void SetCameraPivotYawRotationInverted(bool inverted);
SANDBOX_API bool CameraOrbitYawRotationInverted();
SANDBOX_API void SetCameraOrbitYawRotationInverted(bool inverted);
SANDBOX_API bool CameraPanInvertedX();
SANDBOX_API void SetCameraPanInvertedX(bool inverted);
@ -119,8 +119,8 @@ namespace SandboxEditor
SANDBOX_API AzFramework::InputChannelId CameraTranslateBoostChannelId();
SANDBOX_API void SetCameraTranslateBoostChannelId(AZStd::string_view cameraTranslateBoostId);
SANDBOX_API AzFramework::InputChannelId CameraPivotChannelId();
SANDBOX_API void SetCameraPivotChannelId(AZStd::string_view cameraPivotId);
SANDBOX_API AzFramework::InputChannelId CameraOrbitChannelId();
SANDBOX_API void SetCameraOrbitChannelId(AZStd::string_view cameraOrbitId);
SANDBOX_API AzFramework::InputChannelId CameraFreeLookChannelId();
SANDBOX_API void SetCameraFreeLookChannelId(AZStd::string_view cameraFreeLookId);
@ -128,12 +128,15 @@ namespace SandboxEditor
SANDBOX_API AzFramework::InputChannelId CameraFreePanChannelId();
SANDBOX_API void SetCameraFreePanChannelId(AZStd::string_view cameraFreePanId);
SANDBOX_API AzFramework::InputChannelId CameraPivotLookChannelId();
SANDBOX_API void SetCameraPivotLookChannelId(AZStd::string_view cameraPivotLookId);
SANDBOX_API AzFramework::InputChannelId CameraOrbitLookChannelId();
SANDBOX_API void SetCameraOrbitLookChannelId(AZStd::string_view cameraOrbitLookId);
SANDBOX_API AzFramework::InputChannelId CameraPivotDollyChannelId();
SANDBOX_API void SetCameraPivotDollyChannelId(AZStd::string_view cameraPivotDollyId);
SANDBOX_API AzFramework::InputChannelId CameraOrbitDollyChannelId();
SANDBOX_API void SetCameraOrbitDollyChannelId(AZStd::string_view cameraOrbitDollyId);
SANDBOX_API AzFramework::InputChannelId CameraPivotPanChannelId();
SANDBOX_API void SetCameraPivotPanChannelId(AZStd::string_view cameraPivotPanId);
SANDBOX_API AzFramework::InputChannelId CameraOrbitPanChannelId();
SANDBOX_API void SetCameraOrbitPanChannelId(AZStd::string_view cameraOrbitPanId);
SANDBOX_API AzFramework::InputChannelId CameraFocusChannelId();
SANDBOX_API void SetCameraFocusChannelId(AZStd::string_view cameraFocusId);
} // namespace SandboxEditor

@ -173,6 +173,7 @@ namespace AZ
if (assetTracker)
{
assetTracker->FixUpAsset(*instance);
assetTracker->AddAsset(*instance);
}
@ -185,7 +186,20 @@ namespace AZ
return context.Report(result, message);
}
void SerializedAssetTracker::AddAsset(Asset<AssetData>& asset)
void SerializedAssetTracker::SetAssetFixUp(AssetFixUp assetFixUpCallback)
{
m_assetFixUpCallback = AZStd::move(assetFixUpCallback);
}
void SerializedAssetTracker::FixUpAsset(Asset<AssetData>& asset)
{
if (m_assetFixUpCallback)
{
m_assetFixUpCallback(asset);
}
}
void SerializedAssetTracker::AddAsset(Asset<AssetData> asset)
{
m_serializedAssets.emplace_back(asset);
}
@ -199,5 +213,6 @@ namespace AZ
{
return m_serializedAssets;
}
} // namespace Data
} // namespace AZ

@ -39,13 +39,18 @@ namespace AZ
{
public:
AZ_RTTI(SerializedAssetTracker, "{1E067091-8C0A-44B1-A455-6E97663F6963}");
using AssetFixUp = AZStd::function<void(Asset<AssetData>& asset)>;
void AddAsset(Asset<AssetData>& asset);
void SetAssetFixUp(AssetFixUp assetFixUpCallback);
void FixUpAsset(Asset<AssetData>& asset);
void AddAsset(Asset<AssetData> asset);
AZStd::vector<Asset<AssetData>>& GetTrackedAssets();
const AZStd::vector<Asset<AssetData>>& GetTrackedAssets() const;
private:
AZStd::vector<Asset<AssetData>> m_serializedAssets;
AssetFixUp m_assetFixUpCallback;
};
} // namespace Data
} // namespace AZ

@ -476,15 +476,16 @@ namespace AZ
// Responsible for using the Json Serialization Issue Callback system
// to determine when a JSON Patch or JSON Merge Patch modifies a value
// at a path underneath the IConsole::ConsoleRootCommandKey JSON pointer
// at a path underneath the IConsole::ConsoleRuntimeCommandKey JSON pointer
JsonSerializationResult::ResultCode operator()(AZStd::string_view message,
JsonSerializationResult::ResultCode result, AZStd::string_view path)
{
AZ::IO::PathView consoleRootCommandKey{ IConsole::ConsoleRootCommandKey, AZ::IO::PosixPathSeparator };
constexpr AZ::IO::PathView consoleRootCommandKey{ IConsole::ConsoleRuntimeCommandKey, AZ::IO::PosixPathSeparator };
constexpr AZ::IO::PathView consoleAutoexecCommandKey{ IConsole::ConsoleAutoexecCommandKey, AZ::IO::PosixPathSeparator };
AZ::IO::PathView inputKey{ path, AZ::IO::PosixPathSeparator };
if (result.GetTask() == JsonSerializationResult::Tasks::Merge
&& result.GetProcessing() == JsonSerializationResult::Processing::Completed
&& inputKey.IsRelativeTo(consoleRootCommandKey))
&& (inputKey.IsRelativeTo(consoleRootCommandKey) || inputKey.IsRelativeTo(consoleAutoexecCommandKey)))
{
if (auto type = m_settingsRegistry.GetType(path); type != SettingsRegistryInterface::Type::NoType)
{
@ -510,12 +511,24 @@ namespace AZ
{
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
AZ::IO::PathView consoleRootCommandKey{ IConsole::ConsoleRootCommandKey, AZ::IO::PosixPathSeparator };
constexpr AZ::IO::PathView consoleRuntimeCommandKey{ IConsole::ConsoleRuntimeCommandKey, AZ::IO::PosixPathSeparator };
constexpr AZ::IO::PathView consoleAutoexecCommandKey{ IConsole::ConsoleAutoexecCommandKey, AZ::IO::PosixPathSeparator };
AZ::IO::PathView inputKey{ path, AZ::IO::PosixPathSeparator };
// The ConsoleRootComamndKey is not a command itself so strictly children keys are being examined
if (inputKey.IsRelativeTo(consoleRootCommandKey) && inputKey != consoleRootCommandKey)
// Abuses the IsRelativeToFuncton function of the path class to extract the console
// command from the settings registry objects
FixedValueString command;
if (inputKey != consoleRuntimeCommandKey && inputKey.IsRelativeTo(consoleRuntimeCommandKey))
{
command = inputKey.LexicallyRelative(consoleRuntimeCommandKey).Native();
}
else if (inputKey != consoleAutoexecCommandKey && inputKey.IsRelativeTo(consoleAutoexecCommandKey))
{
command = inputKey.LexicallyRelative(consoleAutoexecCommandKey).Native();
}
if (!command.empty())
{
FixedValueString command = inputKey.LexicallyRelative(consoleRootCommandKey).Native();
ConsoleCommandContainer commandArgs;
// Argument string which stores the value from the Settings Registry long enough
// to pass into the PerformCommand. The ConsoleCommandContainer stores string_views
@ -603,9 +616,10 @@ namespace AZ
void Console::RegisterCommandInvokerWithSettingsRegistry(AZ::SettingsRegistryInterface& settingsRegistry)
{
// Make sure the there is a JSON object at the path of AZ::IConsole::ConsoleRootCommandKey
// Make sure the there is a JSON object at the ConsoleRuntimeCommandKey or ConsoleAutoexecKey
// So that JSON Patch is able to add values underneath that object (JSON Patch doesn't create intermediate objects)
settingsRegistry.MergeSettings(R"({ "Amazon": { "AzCore": { "Runtime": { "ConsoleCommands": {} } }}})",
settingsRegistry.MergeSettings(R"({ "Amazon": { "AzCore": { "Runtime": { "ConsoleCommands": {} } } })"
R"(,"O3DE": { "Autoexec": { "ConsoleCommands": {} } } })",
SettingsRegistryInterface::Format::JsonMergePatch);
m_consoleCommandKeyHandler = settingsRegistry.RegisterNotifier(ConsoleCommandKeyNotificationHandler{ settingsRegistry, *this });

@ -31,7 +31,8 @@ namespace AZ
using FunctorVisitor = AZStd::function<void(ConsoleFunctorBase*)>;
inline static constexpr AZStd::string_view ConsoleRootCommandKey = "/Amazon/AzCore/Runtime/ConsoleCommands";
inline static constexpr AZStd::string_view ConsoleRuntimeCommandKey = "/Amazon/AzCore/Runtime/ConsoleCommands";
inline static constexpr AZStd::string_view ConsoleAutoexecCommandKey = "/O3DE/Autoexec/ConsoleCommands";
IConsole() = default;
virtual ~IConsole() = default;

@ -224,6 +224,8 @@ namespace AZ
void Debug::Trace::Terminate(int exitCode)
{
AZ_TracePrintf("Exit", "Called Terminate() with exit code: 0x%x", exitCode);
AZ::Debug::Trace::PrintCallstack("Exit");
Platform::Terminate(exitCode);
}

@ -160,8 +160,8 @@ namespace AZ
/**
* Locking primitive that is used when executing events in the event queue.
*/
using EventQueueMutexType = typename AZStd::Utils::if_c<AZStd::is_same<typename Traits::EventQueueMutexType, NullMutex>::value, // if EventQueueMutexType==NullMutex use MutexType otherwise EventQueueMutexType
MutexType, typename Traits::EventQueueMutexType>::type;
using EventQueueMutexType = AZStd::conditional_t<AZStd::is_same<typename Traits::EventQueueMutexType, NullMutex>::value, // if EventQueueMutexType==NullMutex use MutexType otherwise EventQueueMutexType
MutexType, typename Traits::EventQueueMutexType>;
/**
* Pointer to an address on the bus.
@ -180,14 +180,22 @@ namespace AZ
* `<BusName>::ExecuteQueuedEvents()`.
* By default, the event queue is disabled.
*/
static const bool EnableEventQueue = Traits::EnableEventQueue;
static const bool EventQueueingActiveByDefault = Traits::EventQueueingActiveByDefault;
static const bool EnableQueuedReferences = Traits::EnableQueuedReferences;
static constexpr bool EnableEventQueue = Traits::EnableEventQueue;
static constexpr bool EventQueueingActiveByDefault = Traits::EventQueueingActiveByDefault;
static constexpr bool EnableQueuedReferences = Traits::EnableQueuedReferences;
/**
* True if the EBus supports more than one address. Otherwise, false.
*/
static const bool HasId = Traits::AddressPolicy != EBusAddressPolicy::Single;
static constexpr bool HasId = Traits::AddressPolicy != EBusAddressPolicy::Single;
/**
* Template Lock Guard class that wraps around the Mutex
* The EBus uses for Dispatching Events.
* This is not the EBus Context Mutex if LocklessDispatch is true
*/
template <typename DispatchMutex>
using DispatchLockGuard = typename Traits::template DispatchLockGuard<DispatchMutex, Traits::LocklessDispatch>;
};
/**
@ -460,7 +468,7 @@ namespace AZ
using BusPtr = typename Traits::BusPtr;
/**
* Helper to queue an event by BusIdType only when function queueing is enabled
* Helper to queue an event by BusIdType only when function queueing is enabled
* @param id Address ID. Handlers that are connected to this ID will receive the event.
* @param func Function pointer of the event to dispatch.
* @param args Function arguments that are passed to each handler.
@ -581,7 +589,7 @@ namespace AZ
, public EBusBroadcaster<Bus, Traits>
, public EBusEventer<Bus, Traits>
, public EBusEventEnumerator<Bus, Traits>
, public AZStd::Utils::if_c<Traits::EnableEventQueue, EBusEventQueue<Bus, Traits>, EBusNullQueue>::type
, public AZStd::conditional_t<Traits::EnableEventQueue, EBusEventQueue<Bus, Traits>, EBusNullQueue>
{
};
@ -599,7 +607,7 @@ namespace AZ
: public EventDispatcher<Bus, Traits>
, public EBusBroadcaster<Bus, Traits>
, public EBusBroadcastEnumerator<Bus, Traits>
, public AZStd::Utils::if_c<Traits::EnableEventQueue, EBusBroadcastQueue<Bus, Traits>, EBusNullQueue>::type
, public AZStd::conditional_t<Traits::EnableEventQueue, EBusBroadcastQueue<Bus, Traits>, EBusNullQueue>
{
};

@ -236,6 +236,17 @@ namespace AZ
* code before or after an event.
*/
using EventProcessingPolicy = EBusEventProcessingPolicy;
/**
* Template Lock Guard class that wraps around the Mutex
* The EBus Context uses the LockGuard when dispatching
* (either AZStd::scoped_lock<MutexType> or NullLockGuard<MutexType>)
* The IsLocklessDispatch bool is there to defer evaluation of the LocklessDispatch constant
* Otherwise the value above in EBusTraits.h is always used and not the value
* that the derived trait class sets.
*/
template <typename DispatchMutex, bool IsLocklessDispatch>
using DispatchLockGuard = AZStd::conditional_t<IsLocklessDispatch, AZ::Internal::NullLockGuard<DispatchMutex>, AZStd::scoped_lock<DispatchMutex>>;
};
namespace Internal
@ -496,6 +507,14 @@ namespace AZ
*/
static const bool HasId = Traits::AddressPolicy != EBusAddressPolicy::Single;
/**
* Template Lock Guard class that wraps around the Mutex
* The EBus uses for Dispatching Events.
* This is not EBus Context Mutex when LocklessDispatch is set
*/
template <typename DispatchMutex>
using DispatchLockGuard = typename ImplTraits::template DispatchLockGuard<DispatchMutex>;
//////////////////////////////////////////////////////////////////////////
// Check to help identify common mistakes
/// @cond EXCLUDE_DOCS
@ -620,11 +639,11 @@ namespace AZ
using ContextMutexType = AZStd::conditional_t<BusTraits::LocklessDispatch && AZStd::is_same_v<MutexType, AZ::NullMutex>, AZStd::shared_mutex, MutexType>;
/**
* The scoped lock guard to use (either AZStd::scoped_lock<MutexType> or NullLockGuard<MutexType>
* The scoped lock guard to use
* during broadcast/event dispatch.
* @see EBusTraits::LocklessDispatch
*/
using DispatchLockGuard = AZStd::conditional_t<BusTraits::LocklessDispatch, AZ::Internal::NullLockGuard<ContextMutexType>, AZStd::scoped_lock<ContextMutexType>>;
using DispatchLockGuard = DispatchLockGuard<ContextMutexType>;
/**
* The scoped lock guard to use during connection. Some specialized policies execute handler methods which
@ -704,6 +723,11 @@ namespace AZ
static Context& GetOrCreateContext(bool trackCallstack=true);
static bool IsInDispatch(Context* context = GetContext(false));
/**
* Returns whether the EBus context is in the middle of a dispatch on the current thread
*/
static bool IsInDispatchThisThread(Context* context = GetContext(false));
/// @cond EXCLUDE_DOCS
struct RouterCallstackEntry
: public CallstackEntry
@ -1208,6 +1232,13 @@ AZ_POP_DISABLE_WARNING
return context != nullptr && context->m_dispatches > 0;
}
template<class Interface, class Traits>
bool EBus<Interface, Traits>::IsInDispatchThisThread(Context* context)
{
return context != nullptr && context->s_callstack != nullptr
&& context->s_callstack->m_prev != nullptr;
}
//=========================================================================
template<class Interface, class Traits>
EBus<Interface, Traits>::RouterCallstackEntry::RouterCallstackEntry(Iterator it, const BusIdType* busId, bool isQueued, bool isReverse)

@ -18,6 +18,14 @@
#include <AzCore/std/parallel/thread.h>
#include <AzCore/Math/MathUtils.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Threading/ThreadUtils.h>
AZ_CVAR(float, cl_jobThreadsConcurrencyRatio, 0.6f, nullptr, AZ::ConsoleFunctorFlags::Null, "Legacy Job system multiplier on the number of hw threads the machine creates at initialization");
AZ_CVAR(uint32_t, cl_jobThreadsNumReserved, 2, nullptr, AZ::ConsoleFunctorFlags::Null, "Legacy Job system number of hardware threads that are reserved for O3DE system threads");
AZ_CVAR(uint32_t, cl_jobThreadsMinNumber, 2, nullptr, AZ::ConsoleFunctorFlags::Null, "Legacy Job system minimum number of worker threads to create after scaling the number of hw threads");
namespace AZ
{
//=========================================================================
@ -46,9 +54,10 @@ namespace AZ
JobManagerThreadDesc threadDesc;
int numberOfWorkerThreads = m_numberOfWorkerThreads;
if (numberOfWorkerThreads <= 0)
if (numberOfWorkerThreads <= 0) // spawn default number of threads
{
numberOfWorkerThreads = AZ::GetMin(static_cast<unsigned int>(desc.m_workerThreads.capacity()), AZStd::thread::hardware_concurrency());
uint32_t scaledHardwareThreads = Threading::CalcNumWorkerThreads(cl_jobThreadsConcurrencyRatio, cl_jobThreadsMinNumber, cl_jobThreadsNumReserved);
numberOfWorkerThreads = AZ::GetMin(static_cast<unsigned int>(desc.m_workerThreads.capacity()), scaledHardwareThreads);
#if (AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS)
numberOfWorkerThreads = AZ::GetMin(numberOfWorkerThreads, AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS);
#endif // (AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS)

@ -36,7 +36,7 @@ namespace AZ
*/
int m_stackSize;
JobManagerThreadDesc(int cpuId = -1, int priority = -100000, int stackSize = -1)
JobManagerThreadDesc(int cpuId = -1, int priority = 0, int stackSize = -1)
: m_cpuId(cpuId)
, m_priority(priority)
, m_stackSize(stackSize)

@ -135,18 +135,12 @@ namespace AZ
{
stringLength = strlen(uuidString);
}
if (stringLength > MaxPermissiveStringSize)
{
if (!skipWarnings)
{
AZ_Warning("Math", false, "Can't create UUID from string length %zu over maximum %zu", stringLength, MaxPermissiveStringSize);
}
return Uuid::CreateNull();
}
size_t newLength{ 0 };
char createString[MaxPermissiveStringSize];
for (size_t curPos = 0; curPos < stringLength; ++curPos)
// Loop until we get to the end of the string OR stop once we've accumulated a full UUID string worth of data
for (size_t curPos = 0; curPos < stringLength && newLength < ValidUuidStringLength; ++curPos)
{
char curChar = uuidString[curPos];
switch (curChar)

@ -42,8 +42,9 @@ namespace AZ
//VER_AZ_RANDOM_CRC32 = 6, // 0 1 1 0
};
static constexpr int ValidUuidStringLength = 32; /// Number of characters (data only, no extra formatting) in a valid UUID string
static const size_t MaxStringBuffer = 39; /// 32 Uuid + 4 dashes + 2 brackets + 1 terminate
Uuid() {}
Uuid(const char* string, size_t stringLength = 0) { *this = CreateString(string, stringLength); }

@ -204,14 +204,14 @@ namespace AZ
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent(const PreMergeEventCallback& callback) = 0;
//! Register a function that will be called before a file is merged.
//! @callback The function to call before a file is merged.
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent (PreMergeEventCallback&& callback) = 0;
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback&& callback) = 0;
//! Register a function that will be called after a file is merged.
//! @callback The function to call after a file is merged.
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent(const PostMergeEventCallback& callback) = 0;
//! Register a function that will be called after a file is merged.
//! @callback The function to call after a file is merged.
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent (PostMergeEventCallback&& callback) = 0;
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback&& callback) = 0;
//! Gets the boolean value at the provided path.
//! @param result The target to write the result to.

@ -0,0 +1,136 @@
/*
* 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/Settings/SettingsRegistryVisitorUtils.h>
namespace AZ::SettingsRegistryVisitorUtils
{
// Field Visitor implementation
FieldVisitor::FieldVisitor() = default;
FieldVisitor::FieldVisitor(VisitFieldType visitFieldType)
: m_visitFieldType{ visitFieldType }
{
}
auto FieldVisitor::Traverse(AZStd::string_view path, AZStd::string_view valueName,
VisitAction action, Type type) -> VisitResponse
{
// A default response skip prevents visiting grand children(depth 2 or lower)
VisitResponse visitResponse = VisitResponse::Skip;
if (action == VisitAction::Begin)
{
// Invoke FieldVisitor override if the root path has been set
if (m_rootPath.has_value())
{
Visit(path, valueName, type);
}
// To make sure only the direct children are visited(depth 1)
// set the root path once and set the VisitReponsoe
// to Continue to recurse into is fields
if (!m_rootPath.has_value())
{
bool visitableFieldType{};
switch (m_visitFieldType)
{
case VisitFieldType::Array:
visitableFieldType = type == Type::Array;
break;
case VisitFieldType::Object:
visitableFieldType = type == Type::Object;
break;
case VisitFieldType::ArrayOrObject:
visitableFieldType = type == Type::Array || type ==Type::Object;
break;
default:
AZ_Error("FieldVisitor", false, "The field visitation type value is invalid");
break;
}
if (visitableFieldType)
{
m_rootPath = path;
visitResponse = VisitResponse::Continue;
}
}
}
else if (action == VisitAction::Value)
{
// Invoke FieldVisitor override if the root path has been set
if (m_rootPath.has_value())
{
Visit(path, valueName, type);
}
}
else if (action == VisitAction::End)
{
// Reset m_rootPath back to null when the root path has finished being visited
if (m_rootPath.has_value() && *m_rootPath == path)
{
m_rootPath = AZStd::nullopt;
}
}
return visitResponse;
}
// Array Visitor implementation
ArrayVisitor::ArrayVisitor()
: FieldVisitor(VisitFieldType::Array)
{
}
// Object Visitor implementation
ObjectVisitor::ObjectVisitor()
: FieldVisitor(VisitFieldType::Object)
{
}
// Generic VisitField Callback implemention
template <typename BaseVisitor>
bool VisitFieldCallback(AZ::SettingsRegistryInterface& settingsRegistry, const VisitorCallback& visitCallback, AZStd::string_view path)
{
struct VisitFieldVisitor
: BaseVisitor
{
using BaseVisitor::Visit;
VisitFieldVisitor(const VisitorCallback& visitCallback)
: m_visitCallback{ visitCallback }
{}
void Visit(AZStd::string_view path, AZStd::string_view fieldIndex, typename BaseVisitor::Type type) override
{
m_visitCallback(path, fieldIndex, type);
}
const VisitorCallback& m_visitCallback;
};
VisitFieldVisitor visitor{ visitCallback };
return settingsRegistry.Visit(visitor, path);
}
// VisitField implementation
bool VisitField(AZ::SettingsRegistryInterface& settingsRegistry, const VisitorCallback& visitCallback, AZStd::string_view path)
{
return VisitFieldCallback<FieldVisitor>(settingsRegistry, visitCallback, path);
}
// VisitArray implementation
bool VisitArray(AZ::SettingsRegistryInterface& settingsRegistry, const VisitorCallback& visitCallback, AZStd::string_view path)
{
return VisitFieldCallback<ArrayVisitor>(settingsRegistry, visitCallback, path);
}
// VisitObject implementation
bool VisitObject(AZ::SettingsRegistryInterface& settingsRegistry, const VisitorCallback& visitCallback, AZStd::string_view path)
{
return VisitFieldCallback<ObjectVisitor>(settingsRegistry, visitCallback, path);
}
}

@ -0,0 +1,83 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Settings/SettingsRegistry.h>
namespace AZ::SettingsRegistryVisitorUtils
{
//! Interface for visiting the fields of an array or object
//! To access the values, use the SettingsRegistryInterface Get/GetObject methods
struct FieldVisitor
: public AZ::SettingsRegistryInterface::Visitor
{
using VisitResponse = AZ::SettingsRegistryInterface::VisitResponse;
using VisitAction = AZ::SettingsRegistryInterface::VisitAction;
using Type = AZ::SettingsRegistryInterface::Type;
FieldVisitor();
// Bring the base class visitor functions into scope
using AZ::SettingsRegistryInterface::Visitor::Visit;
virtual void Visit(AZStd::string_view path, AZStd::string_view arrayIndex, Type type) = 0;
protected:
// VisitFieldType is used for filtering the type of referenced by the root path
enum class VisitFieldType
{
Array,
Object,
ArrayOrObject
};
FieldVisitor(const VisitFieldType visitFieldType);
private:
VisitResponse Traverse(AZStd::string_view path, AZStd::string_view valueName,
VisitAction action, Type type) override;
VisitFieldType m_visitFieldType{ VisitFieldType::ArrayOrObject };
AZStd::optional<AZ::SettingsRegistryInterface::FixedValueString> m_rootPath;
};
//! Interface for visiting the fields of an array
//! To access the values, use the SettingsRegistryInterface Get/GetObject methods
struct ArrayVisitor
: public FieldVisitor
{
ArrayVisitor();
};
//! Interface for visiting the fields of an object
//! To access the values, use the SettingsRegistryInterface Get/GetObject methods
struct ObjectVisitor
: public FieldVisitor
{
ObjectVisitor();
};
//! Signature of callback funcition invoked when visiting an element of an array or object
using VisitorCallback = AZStd::function<void(AZStd::string_view path, AZStd::string_view fieldName,
AZ::SettingsRegistryInterface::Type)>;
//! Invokes the visitor callback for each element of either the array or object at @path
//! If @path is not an array or object, then no elements are visited
//! This function will not recurse into children of elements
//! @visitCallback functor that is invoked for each array or object element found
bool VisitField(AZ::SettingsRegistryInterface& settingsRegistry, const VisitorCallback& visitCallback, AZStd::string_view path);
//! Invokes the visitor callback for each element of the array at @path
//! If @path is not an array, then no elements are visited
//! This function will not recurse into children of elements
//! @visitCallback functor that is invoked for each array element found
bool VisitArray(AZ::SettingsRegistryInterface& settingsRegistry, const VisitorCallback& visitCallback, AZStd::string_view path);
//! Invokes the visitor callback for each element of the object at @path
//! If @path is not an object, then no elements are visited
//! This function will not recurse into children of elements
//! @visitCallback functor that is invoked for each object element found
bool VisitObject(AZ::SettingsRegistryInterface& settingsRegistry, const VisitorCallback& visitCallback, AZStd::string_view path);
}

@ -308,11 +308,11 @@ namespace AZ
void TaskExecutor::SetInstance(TaskExecutor* executor)
{
if (!executor)
if (!executor) // allow unsetting the executor
{
s_executor.Reset();
}
else if (!s_executor) // ignore any calls to set after the first (this happens in unit tests that create new system entities)
else if (!s_executor) // ignore any extra executors after the first (this happens during unit tests)
{
s_executor = AZ::Environment::CreateVariable<TaskExecutor*>(s_executorName, executor);
}

@ -11,9 +11,15 @@
#include <AzCore/Task/TaskGraphSystemComponent.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Math/MathUtils.h>
#include <AzCore/Threading/ThreadUtils.h>
// Create a cvar as a central location for experimentation with switching from the Job system to TaskGraph system.
AZ_CVAR(bool, cl_activateTaskGraph, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Flag clients of TaskGraph to switch between jobs/taskgraph (Note does not disable task graph system)");
AZ_CVAR(float, cl_taskGraphThreadsConcurrencyRatio, 1.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "TaskGraph calculate the number of worker threads to spawn by scaling the number of hw threads, value is clamped between 0.0f and 1.0f");
AZ_CVAR(uint32_t, cl_taskGraphThreadsNumReserved, 2, nullptr, AZ::ConsoleFunctorFlags::Null, "TaskGraph number of hardware threads that are reserved for O3DE system threads. Value is clamped between 0 and the number of logical cores in the system");
AZ_CVAR(uint32_t, cl_taskGraphThreadsMinNumber, 2, nullptr, AZ::ConsoleFunctorFlags::Null, "TaskGraph minimum number of worker threads to create after scaling the number of hw threads");
static constexpr uint32_t TaskExecutorServiceCrc = AZ_CRC_CE("TaskExecutorService");
namespace AZ
@ -24,8 +30,8 @@ namespace AZ
if (Interface<TaskGraphActiveInterface>::Get() == nullptr)
{
Interface<TaskGraphActiveInterface>::Register(this);
m_taskExecutor = aznew TaskExecutor();
Interface<TaskGraphActiveInterface>::Register(this); // small window that another thread can try to use taskgraph between this line and the set instance.
m_taskExecutor = aznew TaskExecutor(Threading::CalcNumWorkerThreads(cl_taskGraphThreadsConcurrencyRatio, cl_taskGraphThreadsMinNumber, cl_taskGraphThreadsNumReserved));
TaskExecutor::SetInstance(m_taskExecutor);
}
}

@ -0,0 +1,25 @@
/*
* 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/Threading/ThreadUtils.h>
#include <AzCore/std/parallel/thread.h>
#include <AzCore/Math/MathUtils.h>
namespace AZ::Threading
{
uint32_t CalcNumWorkerThreads(float workerThreadsRatio, uint32_t minNumWorkerThreads, uint32_t reservedNumThreads)
{
const uint32_t maxHardwareThreads = AZStd::thread::hardware_concurrency();
const uint32_t numReservedThreads = AZ::GetMin<uint32_t>(reservedNumThreads, maxHardwareThreads); // protect against num reserved being bigger than the number of hw threads
const uint32_t maxWorkerThreads = maxHardwareThreads - numReservedThreads;
const float requestedWorkerThreads = AZ::GetClamp<float>(workerThreadsRatio, 0.0f, 1.0f) * static_cast<float>(maxWorkerThreads);
const uint32_t requestedWorkerThreadsRounded = AZStd::lround(requestedWorkerThreads);
const uint32_t numWorkerThreads = AZ::GetMax<uint32_t>(minNumWorkerThreads, requestedWorkerThreadsRounded);
return numWorkerThreads;
}
};

@ -0,0 +1,22 @@
/*
* 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/base.h>
namespace AZ::Threading
{
//! Calculates the number of worker threads a system should use based on the number of hardware threads a device has.
//! result = max (minNumWorkerThreads, workerThreadsRatio * (num_hardware_threads - reservedNumThreads))
//! @param workerThreadsRatio scale applied to the calculated maximum number of threads available after reserved threads have been accounted for. Clamped between 0 and 1.
//! @param minNumWorkerThreads minimum value that will be returned. Value is unclamped and can be more than num_hardware_threads.
//! @param reservedNumThreads number of hardware threads to reserve for O3DE system threads. Value clamped to num_hardware_threads.
//! @return number of worker threads for the calling system to allocate
uint32_t CalcNumWorkerThreads(float workerThreadsRatio, uint32_t minNumWorkerThreads, uint32_t reservedNumThreads);
};

@ -566,6 +566,8 @@ set(FILES
Settings/SettingsRegistryMergeUtils.h
Settings/SettingsRegistryScriptUtils.cpp
Settings/SettingsRegistryScriptUtils.h
Settings/SettingsRegistryVisitorUtils.cpp
Settings/SettingsRegistryVisitorUtils.h
State/HSM.cpp
State/HSM.h
Statistics/NamedRunningStatistic.h
@ -639,6 +641,8 @@ set(FILES
Threading/ThreadSafeDeque.inl
Threading/ThreadSafeObject.h
Threading/ThreadSafeObject.inl
Threading/ThreadUtils.h
Threading/ThreadUtils.cpp
Time/ITime.h
Time/TimeSystemComponent.cpp
Time/TimeSystemComponent.h

@ -59,6 +59,10 @@ namespace AZStd
{
priority = desc->m_priority;
}
else
{
priority = SCHED_OTHER;
}
if (desc->m_name)
{
name = desc->m_name;

@ -2088,7 +2088,7 @@ namespace UnitTest
DisconnectNextHandlerByIdImpl multiHandler2;
multiHandler2.BusConnect(DisconnectNextHandlerByIdImpl::firstBusAddress);
multiHandler2.BusConnect(DisconnectNextHandlerByIdImpl::secondBusAddress);
// Set the first handler m_nextHandler field to point to the second handler
multiHandler1.m_nextHandler = &multiHandler2;
@ -2807,7 +2807,7 @@ namespace UnitTest
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(m_val % m_maxSleep));
}
}
void DoConnect() override
{
MyEventGroupBus::Handler::BusConnect(m_id);
@ -2854,7 +2854,7 @@ namespace UnitTest
}
MyEventGroupBus::Event(id, &MyEventGroupBus::Events::Calculate, i, i * 2, i << 4);
LocklessConnectorBus::Event(id, &LocklessConnectorBus::Events::DoDisconnect);
bool failed = (AZStd::find_if(&sentinel[0], end, [](char s) { return s != 0; }) != end);
@ -2891,7 +2891,7 @@ namespace UnitTest
{
MyEventGroupImpl()
{
}
~MyEventGroupImpl() override
@ -3614,7 +3614,7 @@ namespace UnitTest
{
AZStd::this_thread::yield();
}
EXPECT_GE(AZStd::chrono::system_clock::now(), endTime);
};
AZStd::thread connectThread([&connectHandler, &waitHandler]()
@ -3813,7 +3813,7 @@ namespace UnitTest
struct LastHandlerDisconnectHandler
: public LastHandlerDisconnectBus::Handler
{
void OnEvent() override
void OnEvent() override
{
++m_numOnEvents;
BusDisconnect();
@ -3854,7 +3854,7 @@ namespace UnitTest
struct DisconnectAssertHandler
: public DisconnectAssertBus::Handler
{
};
TEST_F(EBus, HandlerDestroyedWithoutDisconnect_Asserts)
@ -3995,6 +3995,191 @@ namespace UnitTest
idTestRequest.Disconnect();
}
// IsInDispatchThisThread
struct IsInThreadDispatchRequests
: AZ::EBusTraits
{
using MutexType = AZStd::recursive_mutex;
};
using IsInThreadDispatchBus = AZ::EBus<IsInThreadDispatchRequests>;
class IsInThreadDispatchHandler
: public IsInThreadDispatchBus::Handler
{};
TEST_F(EBus, InvokingIsInThisThread_ReturnsSuccess_OnlyIfThreadIsInDispatch)
{
IsInThreadDispatchHandler handler;
handler.BusConnect();
auto ThreadDispatcher = [](IsInThreadDispatchRequests*)
{
EXPECT_TRUE(IsInThreadDispatchBus::IsInDispatchThisThread());
auto PerThreadBusDispatch = []()
{
EXPECT_FALSE(IsInThreadDispatchBus::IsInDispatchThisThread());
};
AZStd::array threads{ AZStd::thread(PerThreadBusDispatch), AZStd::thread(PerThreadBusDispatch) };
for (AZStd::thread& thread : threads)
{
thread.join();
}
};
static constexpr size_t ThreadDispatcherIterations = 4;
for (size_t iteration = 0; iteration < ThreadDispatcherIterations; ++iteration)
{
EXPECT_FALSE(IsInThreadDispatchBus::IsInDispatchThisThread());
IsInThreadDispatchBus::Broadcast(ThreadDispatcher);
EXPECT_FALSE(IsInThreadDispatchBus::IsInDispatchThisThread());
}
}
// Thread Dispatch Policy
struct ThreadDispatchTestBusTraits
: AZ::EBusTraits
{
using MutexType = AZStd::recursive_mutex;
struct PostThreadDispatchTestInvoker
{
~PostThreadDispatchTestInvoker();
};
template <typename DispatchMutex>
struct ThreadDispatchTestLockGuard
{
ThreadDispatchTestLockGuard(DispatchMutex& contextMutex)
: m_lock{ contextMutex }
{}
ThreadDispatchTestLockGuard(DispatchMutex& contextMutex, AZStd::adopt_lock_t adopt_lock)
: m_lock{ contextMutex, adopt_lock }
{}
ThreadDispatchTestLockGuard(const ThreadDispatchTestLockGuard&) = delete;
ThreadDispatchTestLockGuard& operator=(const ThreadDispatchTestLockGuard&) = delete;
private:
PostThreadDispatchTestInvoker m_threadPolicyInvoker;
using LockType = AZStd::conditional_t<LocklessDispatch, AZ::Internal::NullLockGuard<DispatchMutex>, AZStd::scoped_lock<DispatchMutex>>;
LockType m_lock;
};
template <typename DispatchMutex, bool IsLocklessDispatch>
using DispatchLockGuard = ThreadDispatchTestLockGuard<DispatchMutex>;
static inline AZStd::atomic<int32_t> s_threadPostDispatchCalls;
};
class ThreadDispatchTestRequests
{
public:
virtual void FirstCall() = 0;
virtual void SecondCall() = 0;
virtual void ThirdCall() = 0;
};
using ThreadDispatchTestBus = AZ::EBus<ThreadDispatchTestRequests, ThreadDispatchTestBusTraits>;
ThreadDispatchTestBusTraits::PostThreadDispatchTestInvoker::~PostThreadDispatchTestInvoker()
{
if (!ThreadDispatchTestBus::IsInDispatchThisThread())
{
++s_threadPostDispatchCalls;
}
}
class ThreadDispatchTestHandler
: public ThreadDispatchTestBus::Handler
{
public:
void Connect()
{
ThreadDispatchTestBus::Handler::BusConnect();
}
void Disconnect()
{
ThreadDispatchTestBus::Handler::BusDisconnect();
}
void FirstCall() override
{
ThreadDispatchTestBus::Broadcast(&ThreadDispatchTestBus::Events::SecondCall);
}
void SecondCall() override
{
ThreadDispatchTestBus::Broadcast(&ThreadDispatchTestBus::Events::ThirdCall);
}
void ThirdCall() override
{
}
};
template <typename ParamType>
class EBusParamFixture
: public ScopedAllocatorSetupFixture
, public ::testing::WithParamInterface<ParamType>
{};
struct ThreadDispatchParams
{
size_t m_threadCount{};
size_t m_handlerCount{};
};
using ThreadDispatchParamFixture = EBusParamFixture<ThreadDispatchParams>;
INSTANTIATE_TEST_CASE_P(
ThreadDispatch,
ThreadDispatchParamFixture,
::testing::Values(
ThreadDispatchParams{ 1, 1 },
ThreadDispatchParams{ 2, 1 },
ThreadDispatchParams{ 1, 2 },
ThreadDispatchParams{ 2, 2 },
ThreadDispatchParams{ 16, 8 }
)
);
TEST_P(ThreadDispatchParamFixture, CustomDispatchLockGuard_InvokesPostDispatchFunction_AfterThreadHasFinishedDispatch)
{
ThreadDispatchTestBusTraits::s_threadPostDispatchCalls = 0;
ThreadDispatchParams threadDispatchParams = GetParam();
AZStd::vector<AZStd::thread> testThreads;
AZStd::vector<ThreadDispatchTestHandler> testHandlers(threadDispatchParams.m_handlerCount);
for (ThreadDispatchTestHandler& testHandler : testHandlers)
{
testHandler.Connect();
}
static constexpr size_t DispatchThreadCalls = 3;
const size_t totalThreadDispatchCalls = threadDispatchParams.m_threadCount * DispatchThreadCalls;
auto DispatchThreadWorker = []()
{
ThreadDispatchTestBus::Broadcast(&ThreadDispatchTestBus::Events::FirstCall);
ThreadDispatchTestBus::Broadcast(&ThreadDispatchTestBus::Events::SecondCall);
ThreadDispatchTestBus::Broadcast(&ThreadDispatchTestBus::Events::ThirdCall);
};
for (size_t threadIndex = 0; threadIndex < threadDispatchParams.m_threadCount; ++threadIndex)
{
testThreads.emplace_back(DispatchThreadWorker);
}
for (AZStd::thread& thread : testThreads)
{
thread.join();
}
for (ThreadDispatchTestHandler& testHandler : testHandlers)
{
testHandler.Disconnect();
}
EXPECT_EQ(totalThreadDispatchCalls, ThreadDispatchTestBusTraits::s_threadPostDispatchCalls);
ThreadDispatchTestBusTraits::s_threadPostDispatchCalls = 0;
}
} // namespace UnitTest
#if defined(HAVE_BENCHMARK)
@ -4370,7 +4555,7 @@ namespace Benchmark
Bus::ExecuteQueuedEvents();
}
s_benchmarkEBusEnv<Bus>.Disconnect(state);
}
BUS_BENCHMARK_REGISTER_ALL(BM_EBus_ExecuteBroadcast);

@ -0,0 +1,196 @@
/*
* 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/Settings/SettingsRegistryImpl.h>
#include <AzCore/Settings/SettingsRegistryVisitorUtils.h>
#include <AzCore/std/containers/fixed_vector.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/std/string/string.h>
#include <AzCore/UnitTest/TestTypes.h>
namespace SettingsRegistryVisitorUtilsTests
{
struct VisitCallbackParams
{
AZStd::string_view m_inputJsonDocument;
using VisitFieldFunction = bool(*)(AZ::SettingsRegistryInterface&,
const AZ::SettingsRegistryVisitorUtils::VisitorCallback&,
AZStd::string_view);
static inline constexpr size_t MaxFieldCount = 10;
using ObjectFields = AZStd::fixed_vector<AZStd::pair<AZStd::string_view, AZStd::string_view>, MaxFieldCount>;
using ArrayFields = AZStd::fixed_vector<AZStd::string_view, MaxFieldCount>;
ObjectFields m_objectFields;
ArrayFields m_arrayFields;
};
template <typename VisitorParams>
class SettingsRegistryVisitorUtilsParamFixture
: public UnitTest::ScopedAllocatorSetupFixture
, public ::testing::WithParamInterface<VisitorParams>
{
public:
void SetUp() override
{
m_registry = AZStd::make_unique<AZ::SettingsRegistryImpl>();
}
void TearDown() override
{
m_registry.reset();
}
AZStd::unique_ptr<AZ::SettingsRegistryImpl> m_registry;
};
using SettingsRegistryVisitCallbackFixture = SettingsRegistryVisitorUtilsParamFixture<VisitCallbackParams>;
TEST_P(SettingsRegistryVisitCallbackFixture, VisitFunction_VisitFieldsOfArrayType_ReturnsFields)
{
const VisitCallbackParams& visitParams = GetParam();
ASSERT_TRUE(m_registry->MergeSettings(visitParams.m_inputJsonDocument, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
AZStd::fixed_vector<AZStd::string, VisitCallbackParams::MaxFieldCount> testArrayFields;
auto visitorCallback = [this, &testArrayFields](AZStd::string_view path, AZStd::string_view, AZ::SettingsRegistryInterface::Type)
{
AZStd::string fieldValue;
EXPECT_TRUE(m_registry->Get(fieldValue, path));
testArrayFields.emplace_back(AZStd::move(fieldValue));
};
AZ::SettingsRegistryVisitorUtils::VisitField(*m_registry, visitorCallback, "/Test/Array");
const AZStd::fixed_vector<AZStd::string, VisitCallbackParams::MaxFieldCount> expectedFields{
visitParams.m_arrayFields.begin(), visitParams.m_arrayFields.end() };
EXPECT_THAT(testArrayFields, ::testing::ContainerEq(expectedFields));
}
TEST_P(SettingsRegistryVisitCallbackFixture, VisitFunction_VisitFieldsOfObjectType_ReturnsFields)
{
const VisitCallbackParams& visitParams = GetParam();
ASSERT_TRUE(m_registry->MergeSettings(visitParams.m_inputJsonDocument, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
AZStd::fixed_vector<AZStd::pair<AZStd::string, AZStd::string>, VisitCallbackParams::MaxFieldCount> testObjectFields;
auto visitorCallback = [this, &testObjectFields](AZStd::string_view path, AZStd::string_view fieldName, AZ::SettingsRegistryInterface::Type)
{
AZStd::string fieldValue;
EXPECT_TRUE(m_registry->Get(fieldValue, path));
testObjectFields.emplace_back(fieldName, AZStd::move(fieldValue));
};
AZ::SettingsRegistryVisitorUtils::VisitField(*m_registry, visitorCallback, "/Test/Object");
const AZStd::fixed_vector<AZStd::pair<AZStd::string, AZStd::string>, VisitCallbackParams::MaxFieldCount> expectedFields{
visitParams.m_objectFields.begin(), visitParams.m_objectFields.end() };
EXPECT_THAT(testObjectFields, ::testing::ContainerEq(expectedFields));
}
TEST_P(SettingsRegistryVisitCallbackFixture, VisitFunction_VisitArrayOfArrayType_ReturnsFields)
{
const VisitCallbackParams& visitParams = GetParam();
ASSERT_TRUE(m_registry->MergeSettings(visitParams.m_inputJsonDocument, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
AZStd::fixed_vector<AZStd::string, VisitCallbackParams::MaxFieldCount> testArrayFields;
auto visitorCallback = [this, &testArrayFields](AZStd::string_view path, AZStd::string_view, AZ::SettingsRegistryInterface::Type)
{
AZStd::string fieldValue;
EXPECT_TRUE(m_registry->Get(fieldValue, path));
testArrayFields.emplace_back(AZStd::move(fieldValue));
};
AZ::SettingsRegistryVisitorUtils::VisitArray(*m_registry, visitorCallback, "/Test/Array");
const AZStd::fixed_vector<AZStd::string, VisitCallbackParams::MaxFieldCount> expectedArrayFields{
visitParams.m_arrayFields.begin(), visitParams.m_arrayFields.end() };
EXPECT_THAT(testArrayFields, ::testing::ContainerEq(expectedArrayFields));
}
TEST_P(SettingsRegistryVisitCallbackFixture, VisitFunction_VisitArrayOfObjectType_ReturnsEmpty)
{
const VisitCallbackParams& visitParams = GetParam();
ASSERT_TRUE(m_registry->MergeSettings(visitParams.m_inputJsonDocument, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
AZStd::fixed_vector<AZStd::string, VisitCallbackParams::MaxFieldCount> testArrayFields;
auto visitorCallback = [this, &testArrayFields](AZStd::string_view path, AZStd::string_view, AZ::SettingsRegistryInterface::Type)
{
AZStd::string fieldValue;
EXPECT_TRUE(m_registry->Get(fieldValue, path));
testArrayFields.emplace_back(AZStd::move(fieldValue));
};
AZ::SettingsRegistryVisitorUtils::VisitArray(*m_registry, visitorCallback, "/Test/Object");
EXPECT_TRUE(testArrayFields.empty());
}
TEST_P(SettingsRegistryVisitCallbackFixture, VisitFunction_VisitObjectOfArrayType_ReturnsEmpty)
{
const VisitCallbackParams& visitParams = GetParam();
ASSERT_TRUE(m_registry->MergeSettings(visitParams.m_inputJsonDocument, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
AZStd::fixed_vector<AZStd::pair<AZStd::string, AZStd::string>, VisitCallbackParams::MaxFieldCount> testObjectFields;
auto visitorCallback = [this, &testObjectFields](AZStd::string_view path, AZStd::string_view fieldName, AZ::SettingsRegistryInterface::Type)
{
AZStd::string fieldValue;
EXPECT_TRUE(m_registry->Get(fieldValue, path));
testObjectFields.emplace_back(fieldName, AZStd::move(fieldValue));
};
AZ::SettingsRegistryVisitorUtils::VisitObject(*m_registry, visitorCallback, "/Test/Array");
EXPECT_TRUE(testObjectFields.empty());
}
TEST_P(SettingsRegistryVisitCallbackFixture, VisitFunction_VisitObjectOfObjectType_ReturnsFields)
{
const VisitCallbackParams& visitParams = GetParam();
ASSERT_TRUE(m_registry->MergeSettings(visitParams.m_inputJsonDocument, AZ::SettingsRegistryInterface::Format::JsonMergePatch));
AZStd::fixed_vector<AZStd::pair<AZStd::string, AZStd::string>, VisitCallbackParams::MaxFieldCount> testObjectFields;
auto visitorCallback = [this, &testObjectFields](AZStd::string_view path, AZStd::string_view fieldName, AZ::SettingsRegistryInterface::Type)
{
AZStd::string fieldValue;
EXPECT_TRUE(m_registry->Get(fieldValue, path));
testObjectFields.emplace_back(fieldName, AZStd::move(fieldValue));
};
AZ::SettingsRegistryVisitorUtils::VisitObject(*m_registry, visitorCallback, "/Test/Object");
const AZStd::fixed_vector<AZStd::pair<AZStd::string, AZStd::string>, VisitCallbackParams::MaxFieldCount> expectedObjectFields{
visitParams.m_objectFields.begin(), visitParams.m_objectFields.end() };
EXPECT_THAT(testObjectFields, ::testing::ContainerEq(expectedObjectFields));
}
INSTANTIATE_TEST_CASE_P(
VisitField,
SettingsRegistryVisitCallbackFixture,
::testing::Values(
VisitCallbackParams
{
R"({)" "\n"
R"( "Test":)" "\n"
R"( {)" "\n"
R"( "Array": [ "Hello", "World" ],)" "\n"
R"( "Object": { "Foo": "Hello", "Bar": "World"})" "\n"
R"( })" "\n"
R"(})" "\n",
VisitCallbackParams::ObjectFields{{"Foo", "Hello"}, {"Bar", "World"}},
VisitCallbackParams::ArrayFields{"Hello", "World"}
}
)
);
}

@ -247,4 +247,27 @@ namespace UnitTest
Uuid right = Uuid::CreateStringPermissive(permissiveStr1);
EXPECT_EQ(left, right);
}
TEST_F(UuidTests, CreateStringPermissive_StringWithExtraData_Succeeds)
{
const char uuidStr[] = "{34D44249-E599-4B30-811F-4215C2DEA269}";
Uuid left = Uuid::CreateString(uuidStr);
const char permissiveStr[] = "0x34D44249-0xE5994B30-0x811F4215-0xC2DEA269 Hello World";
Uuid right = Uuid::CreateStringPermissive(permissiveStr);
EXPECT_EQ(left, right);
}
TEST_F(UuidTests, CreateStringPermissive_StringWithLotsOfExtraData_Succeeds)
{
const char uuidStr[] = "{34D44249-E599-4B30-811F-4215C2DEA269}";
Uuid left = Uuid::CreateString(uuidStr);
const char permissiveStr[] = "0x34D44249-0xE5994B30-0x811F4215-0xC2DEA269 Hello World this is a really long string "
"with lots of extra data to make sure we can parse a long string without failing as long as the uuid is in "
"the beginning of the string then we should succeed";
Uuid right = Uuid::CreateStringPermissive(permissiveStr);
EXPECT_EQ(left, right);
}
}

@ -75,11 +75,12 @@ set(FILES
Name/NameJsonSerializerTests.cpp
Name/NameTests.cpp
RTTI/TypeSafeIntegralTests.cpp
SettingsRegistryTests.cpp
SettingsRegistryMergeUtilsTests.cpp
Settings/CommandLineTests.cpp
Settings/SettingsRegistryTests.cpp
Settings/SettingsRegistryConsoleUtilsTests.cpp
Settings/SettingsRegistryMergeUtilsTests.cpp
Settings/SettingsRegistryScriptUtilsTests.cpp
Settings/SettingsRegistryVisitorUtilsTests.cpp
Streamer/BlockCacheTests.cpp
Streamer/DedicatedCacheTests.cpp
Streamer/FullDecompressorTests.cpp

@ -133,6 +133,8 @@ namespace AzFramework
behaviorContext->Class<BehaviorEntity>("Entity")
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
->Attribute(AZ::Script::Attributes::ConstructorOverride, &Internal::BehaviorEntityScriptConstructor)
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "entity")
->Constructor()
->Constructor<AZ::EntityId>()
->Constructor<AZ::Entity*>()

@ -0,0 +1,63 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/std/string/string.h>
#include <AzFramework/Matchmaking/MatchmakingRequests.h>
namespace AzFramework
{
//! IMatchmakingRequests
//! Pure virtual session interface class to abstract the details of session handling from application code.
class IMatchmakingRequests
{
public:
AZ_RTTI(IMatchmakingRequests, "{BC0B74DA-A448-4F40-9B50-9D73142829D5}");
IMatchmakingRequests() = default;
virtual ~IMatchmakingRequests() = default;
// Registers a player's acceptance or rejection of a proposed matchmaking.
// @param acceptMatchRequest The request of AcceptMatch operation
virtual void AcceptMatch(const AcceptMatchRequest& acceptMatchRequest) = 0;
// Create a game match for a group of players.
// @param startMatchmakingRequest The request of StartMatchmaking operation
// @return A unique identifier for a matchmaking ticket
virtual AZStd::string StartMatchmaking(const StartMatchmakingRequest& startMatchmakingRequest) = 0;
// Cancels a matchmaking ticket that is currently being processed.
// @param stopMatchmakingRequest The request of StopMatchmaking operation
virtual void StopMatchmaking(const StopMatchmakingRequest& stopMatchmakingRequest) = 0;
};
//! IMatchmakingAsyncRequests
//! Async version of IMatchmakingRequests
class IMatchmakingAsyncRequests
{
public:
AZ_RTTI(ISessionAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}");
IMatchmakingAsyncRequests() = default;
virtual ~IMatchmakingAsyncRequests() = default;
// AcceptMatch Async
// @param acceptMatchRequest The request of AcceptMatch operation
virtual void AcceptMatchAsync(const AcceptMatchRequest& acceptMatchRequest) = 0;
// StartMatchmaking Async
// @param startMatchmakingRequest The request of StartMatchmaking operation
virtual void StartMatchmakingAsync(const StartMatchmakingRequest& startMatchmakingRequest) = 0;
// StopMatchmaking Async
// @param stopMatchmakingRequest The request of StopMatchmaking operation
virtual void StopMatchmakingAsync(const StopMatchmakingRequest& stopMatchmakingRequest) = 0;
};
} // namespace AzFramework

@ -0,0 +1,62 @@
/*
* 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/string/string.h>
namespace AzFramework
{
//! MatchmakingAsyncRequestNotifications
//! The notifications correspond to matchmaking async requests
class MatchmakingAsyncRequestNotifications
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes
virtual void OnAcceptMatchAsyncComplete() = 0;
// OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes
// @param matchmakingTicketId The unique identifier for the matchmaking ticket
virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0;
// OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes
virtual void OnStopMatchmakingAsyncComplete() = 0;
};
using MatchmakingAsyncRequestNotificationBus = AZ::EBus<MatchmakingAsyncRequestNotifications>;
//! MatchmakingNotifications
//! The matchmaking notifications to listen for performing required operations
class MatchAcceptanceNotifications
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnMatchAcceptance is fired when DescribeMatchmaking ticket status is REQUIRES_ACCEPTANCE
virtual void OnMatchAcceptance() = 0;
};
using MatchAcceptanceNotificationBus = AZ::EBus<MatchAcceptanceNotifications>;
} // namespace AzFramework

@ -0,0 +1,78 @@
/*
* 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/RTTI/ReflectContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Matchmaking/MatchmakingRequests.h>
namespace AzFramework
{
void AcceptMatchRequest::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<AcceptMatchRequest>()
->Version(0)
->Field("acceptMatch", &AcceptMatchRequest::m_acceptMatch)
->Field("playerIds", &AcceptMatchRequest::m_playerIds)
->Field("ticketId", &AcceptMatchRequest::m_ticketId);
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<AcceptMatchRequest>("AcceptMatchRequest", "The container for AcceptMatch request parameters")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &AcceptMatchRequest::m_acceptMatch, "AcceptMatch",
"Player response to accept or reject match")
->DataElement(AZ::Edit::UIHandlers::Default, &AcceptMatchRequest::m_playerIds, "PlayerIds",
"A list of unique identifiers for players delivering the response")
->DataElement(AZ::Edit::UIHandlers::Default, &AcceptMatchRequest::m_ticketId, "TicketId",
"A unique identifier for a matchmaking ticket");
}
}
}
void StartMatchmakingRequest::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<StartMatchmakingRequest>()
->Version(0)
->Field("ticketId", &StartMatchmakingRequest::m_ticketId);
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<StartMatchmakingRequest>("StartMatchmakingRequest", "The container for StartMatchmaking request parameters")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &StartMatchmakingRequest::m_ticketId, "TicketId",
"A unique identifier for a matchmaking ticket");
}
}
}
void StopMatchmakingRequest::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<StopMatchmakingRequest>()
->Version(0)
->Field("ticketId", &StopMatchmakingRequest::m_ticketId);
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<StopMatchmakingRequest>("StopMatchmakingRequest", "The container for StopMatchmaking request parameters")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &StopMatchmakingRequest::m_ticketId, "TicketId",
"A unique identifier for a matchmaking ticket");
}
}
}
} // namespace AzFramework

@ -0,0 +1,66 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/string/string.h>
namespace AZ
{
class ReflectContext;
}
namespace AzFramework
{
//! AcceptMatchRequest
//! The container for AcceptMatch request parameters.
struct AcceptMatchRequest
{
AZ_RTTI(AcceptMatchRequest, "{AD289D76-CEE2-424F-847E-E62AA83B7D79}");
static void Reflect(AZ::ReflectContext* context);
AcceptMatchRequest() = default;
virtual ~AcceptMatchRequest() = default;
// Player response to accept or reject match
bool m_acceptMatch;
// A list of unique identifiers for players delivering the response
AZStd::vector<AZStd::string> m_playerIds;
// A unique identifier for a matchmaking ticket
AZStd::string m_ticketId;
};
//! StartMatchmakingRequest
//! The container for StartMatchmaking request parameters.
struct StartMatchmakingRequest
{
AZ_RTTI(StartMatchmakingRequest, "{70B47776-E8E7-4993-BEC3-5CAEC3D48E47}");
static void Reflect(AZ::ReflectContext* context);
StartMatchmakingRequest() = default;
virtual ~StartMatchmakingRequest() = default;
// A unique identifier for a matchmaking ticket
AZStd::string m_ticketId;
};
//! StopMatchmakingRequest
//! The container for StopMatchmaking request parameters.
struct StopMatchmakingRequest
{
AZ_RTTI(StopMatchmakingRequest, "{6132E293-65EF-4DC2-A8A0-00269697229D}");
static void Reflect(AZ::ReflectContext* context);
StopMatchmakingRequest() = default;
virtual ~StopMatchmakingRequest() = default;
// A unique identifier for a matchmaking ticket
AZStd::string m_ticketId;
};
} // namespace AzFramework

@ -10,98 +10,11 @@
#include <AzCore/EBus/EBus.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/string/string.h>
#include <AzCore/Outcome/Outcome.h>
#include <AzFramework/Session/SessionRequests.h>
namespace AzFramework
{
struct SessionConfig;
//! CreateSessionRequest
//! The container for CreateSession request parameters.
struct CreateSessionRequest
{
AZ_RTTI(CreateSessionRequest, "{E39C2A45-89C9-4CFB-B337-9734DC798930}");
static void Reflect(AZ::ReflectContext* context);
CreateSessionRequest() = default;
virtual ~CreateSessionRequest() = default;
// A unique identifier for a player or entity creating the session.
AZStd::string m_creatorId;
// A collection of custom properties for a session.
AZStd::unordered_map<AZStd::string, AZStd::string> m_sessionProperties;
// A descriptive label that is associated with a session.
AZStd::string m_sessionName;
// The maximum number of players that can be connected simultaneously to the session.
uint64_t m_maxPlayer = 0;
};
//! SearchSessionsRequest
//! The container for SearchSessions request parameters.
struct SearchSessionsRequest
{
AZ_RTTI(SearchSessionsRequest, "{B49207A8-8549-4ADB-B7D9-D7A4932F9B4B}");
static void Reflect(AZ::ReflectContext* context);
SearchSessionsRequest() = default;
virtual ~SearchSessionsRequest() = default;
// String containing the search criteria for the session search. If no filter expression is included, the request returns results
// for all active sessions.
AZStd::string m_filterExpression;
// Instructions on how to sort the search results. If no sort expression is included, the request returns results in random order.
AZStd::string m_sortExpression;
// The maximum number of results to return.
uint8_t m_maxResult = 0;
// A token that indicates the start of the next sequential page of results.
AZStd::string m_nextToken;
};
//! SearchSessionsResponse
//! The container for SearchSession request results.
struct SearchSessionsResponse
{
AZ_RTTI(SearchSessionsResponse, "{F93DE7DC-D381-4E08-8A3B-0B08F7C38714}");
static void Reflect(AZ::ReflectContext* context);
SearchSessionsResponse() = default;
virtual ~SearchSessionsResponse() = default;
// A collection of sessions that match the search criteria and sorted in specific order.
AZStd::vector<SessionConfig> m_sessionConfigs;
// A token that indicates the start of the next sequential page of results.
AZStd::string m_nextToken;
};
//! JoinSessionRequest
//! The container for JoinSession request parameters.
struct JoinSessionRequest
{
AZ_RTTI(JoinSessionRequest, "{519769E8-3CDE-4385-A0D7-24DBB3685657}");
static void Reflect(AZ::ReflectContext* context);
JoinSessionRequest() = default;
virtual ~JoinSessionRequest() = default;
// A unique identifier for the session.
AZStd::string m_sessionId;
// A unique identifier for a player. Player IDs are developer-defined.
AZStd::string m_playerId;
// Developer-defined information related to a player.
AZStd::string m_playerData;
};
//! ISessionRequests
//! Pure virtual session interface class to abstract the details of session handling from application code.
class ISessionRequests

@ -6,9 +6,10 @@
*
*/
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Session/ISessionRequests.h>
#include <AzFramework/Session/SessionRequests.h>
#include <AzFramework/Session/SessionConfig.h>
namespace AzFramework

@ -0,0 +1,107 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/string/string.h>
namespace AZ
{
class ReflectContext;
}
namespace AzFramework
{
struct SessionConfig;
//! CreateSessionRequest
//! The container for CreateSession request parameters.
struct CreateSessionRequest
{
AZ_RTTI(CreateSessionRequest, "{E39C2A45-89C9-4CFB-B337-9734DC798930}");
static void Reflect(AZ::ReflectContext* context);
CreateSessionRequest() = default;
virtual ~CreateSessionRequest() = default;
// A unique identifier for a player or entity creating the session.
AZStd::string m_creatorId;
// A collection of custom properties for a session.
AZStd::unordered_map<AZStd::string, AZStd::string> m_sessionProperties;
// A descriptive label that is associated with a session.
AZStd::string m_sessionName;
// The maximum number of players that can be connected simultaneously to the session.
uint64_t m_maxPlayer = 0;
};
//! SearchSessionsRequest
//! The container for SearchSessions request parameters.
struct SearchSessionsRequest
{
AZ_RTTI(SearchSessionsRequest, "{B49207A8-8549-4ADB-B7D9-D7A4932F9B4B}");
static void Reflect(AZ::ReflectContext* context);
SearchSessionsRequest() = default;
virtual ~SearchSessionsRequest() = default;
// String containing the search criteria for the session search. If no filter expression is included, the request returns results
// for all active sessions.
AZStd::string m_filterExpression;
// Instructions on how to sort the search results. If no sort expression is included, the request returns results in random order.
AZStd::string m_sortExpression;
// The maximum number of results to return.
uint8_t m_maxResult = 0;
// A token that indicates the start of the next sequential page of results.
AZStd::string m_nextToken;
};
//! SearchSessionsResponse
//! The container for SearchSession request results.
struct SearchSessionsResponse
{
AZ_RTTI(SearchSessionsResponse, "{F93DE7DC-D381-4E08-8A3B-0B08F7C38714}");
static void Reflect(AZ::ReflectContext* context);
SearchSessionsResponse() = default;
virtual ~SearchSessionsResponse() = default;
// A collection of sessions that match the search criteria and sorted in specific order.
AZStd::vector<SessionConfig> m_sessionConfigs;
// A token that indicates the start of the next sequential page of results.
AZStd::string m_nextToken;
};
//! JoinSessionRequest
//! The container for JoinSession request parameters.
struct JoinSessionRequest
{
AZ_RTTI(JoinSessionRequest, "{519769E8-3CDE-4385-A0D7-24DBB3685657}");
static void Reflect(AZ::ReflectContext* context);
JoinSessionRequest() = default;
virtual ~JoinSessionRequest() = default;
// A unique identifier for the session.
AZStd::string m_sessionId;
// A unique identifier for a player. Player IDs are developer-defined.
AZStd::string m_playerId;
// Developer-defined information related to a player.
AZStd::string m_playerData;
};
} // namespace AzFramework

@ -533,8 +533,8 @@ namespace AzFramework
m_translateCameraInputChannelIds = translateCameraInputChannelIds;
}
PivotCameraInput::PivotCameraInput(const InputChannelId& pivotChannelId)
: m_pivotChannelId(pivotChannelId)
OrbitCameraInput::OrbitCameraInput(const InputChannelId& orbitChannelId)
: m_orbitChannelId(orbitChannelId)
{
m_pivotFn = []([[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
{
@ -542,11 +542,11 @@ namespace AzFramework
};
}
bool PivotCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, const float scrollDelta)
bool OrbitCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, const float scrollDelta)
{
if (const auto* input = AZStd::get_if<DiscreteInputEvent>(&event))
{
if (input->m_channelId == m_pivotChannelId)
if (input->m_channelId == m_orbitChannelId)
{
if (input->m_state == InputChannel::State::Began)
{
@ -561,13 +561,13 @@ namespace AzFramework
if (Active())
{
return m_pivotCameras.HandleEvents(event, cursorDelta, scrollDelta);
return m_orbitCameras.HandleEvents(event, cursorDelta, scrollDelta);
}
return !Idle();
}
Camera PivotCameraInput::StepCamera(
Camera OrbitCameraInput::StepCamera(
const Camera& targetCamera, const ScreenVector& cursorDelta, const float scrollDelta, const float deltaTime)
{
Camera nextCamera = targetCamera;
@ -581,12 +581,12 @@ namespace AzFramework
if (Active())
{
MovePivotDetached(nextCamera, m_pivotFn(targetCamera.Translation(), targetCamera.Rotation().GetBasisY()));
nextCamera = m_pivotCameras.StepCamera(nextCamera, cursorDelta, scrollDelta, deltaTime);
nextCamera = m_orbitCameras.StepCamera(nextCamera, cursorDelta, scrollDelta, deltaTime);
}
if (Ending())
{
m_pivotCameras.Reset();
m_orbitCameras.Reset();
nextCamera.m_pivot = nextCamera.Translation();
nextCamera.m_offset = AZ::Vector3::CreateZero();
@ -595,12 +595,12 @@ namespace AzFramework
return nextCamera;
}
void PivotCameraInput::SetPivotInputChannelId(const InputChannelId& pivotChanneId)
void OrbitCameraInput::SetOrbitInputChannelId(const InputChannelId& orbitChanneId)
{
m_pivotChannelId = pivotChanneId;
m_orbitChannelId = orbitChanneId;
}
PivotDollyScrollCameraInput::PivotDollyScrollCameraInput()
OrbitDollyScrollCameraInput::OrbitDollyScrollCameraInput()
{
m_scrollSpeedFn = []() constexpr
{
@ -608,7 +608,7 @@ namespace AzFramework
};
}
bool PivotDollyScrollCameraInput::HandleEvents(
bool OrbitDollyScrollCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
{
if (const auto* scroll = AZStd::get_if<ScrollEvent>(&event))
@ -619,36 +619,45 @@ namespace AzFramework
return !Idle();
}
static Camera PivotDolly(const Camera& targetCamera, const float delta)
static Camera OrbitDolly(const Camera& targetCamera, const float delta)
{
Camera nextCamera = targetCamera;
const auto pivotDirection = targetCamera.m_offset.GetNormalized();
nextCamera.m_offset -= pivotDirection * delta;
const auto pivotDot = targetCamera.m_offset.Dot(nextCamera.m_offset);
const auto distance = nextCamera.m_offset.GetLength() * AZ::GetSign(pivotDot);
// handle case where pivot and offset may be the same to begin with
// choose negative y-axis for offset to default to moving the camera backwards from the pivot (standard centered pivot behavior)
const auto pivotDirection = [&targetCamera]
{
if (const auto offsetLength = targetCamera.m_offset.GetLength(); AZ::IsCloseMag(offsetLength, 0.0f))
{
return -AZ::Vector3::CreateAxisY();
}
else
{
return targetCamera.m_offset / offsetLength;
}
}();
const auto minDistance = 0.01f;
if (distance < minDistance || pivotDot < 0.0f)
nextCamera.m_offset -= pivotDirection * delta;
if (pivotDirection.Dot(nextCamera.m_offset) < 0.0f)
{
nextCamera.m_offset = pivotDirection * minDistance;
nextCamera.m_offset = pivotDirection * 0.001f;
}
return nextCamera;
}
Camera PivotDollyScrollCameraInput::StepCamera(
Camera OrbitDollyScrollCameraInput::StepCamera(
const Camera& targetCamera,
[[maybe_unused]] const ScreenVector& cursorDelta,
const float scrollDelta,
[[maybe_unused]] const float deltaTime)
{
const auto nextCamera = PivotDolly(targetCamera, aznumeric_cast<float>(scrollDelta) * m_scrollSpeedFn());
const auto nextCamera = OrbitDolly(targetCamera, aznumeric_cast<float>(scrollDelta) * m_scrollSpeedFn());
EndActivation();
return nextCamera;
}
PivotDollyMotionCameraInput::PivotDollyMotionCameraInput(const InputChannelId& dollyChannelId)
OrbitDollyMotionCameraInput::OrbitDollyMotionCameraInput(const InputChannelId& dollyChannelId)
: m_dollyChannelId(dollyChannelId)
{
m_motionSpeedFn = []() constexpr
@ -657,28 +666,28 @@ namespace AzFramework
};
}
bool PivotDollyMotionCameraInput::HandleEvents(
bool OrbitDollyMotionCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
{
HandleActivationEvents(event, m_dollyChannelId, cursorDelta, m_clickDetector, *this);
return CameraInputUpdatingAfterMotion(*this);
}
Camera PivotDollyMotionCameraInput::StepCamera(
Camera OrbitDollyMotionCameraInput::StepCamera(
const Camera& targetCamera,
const ScreenVector& cursorDelta,
[[maybe_unused]] const float scrollDelta,
[[maybe_unused]] const float deltaTime)
{
return PivotDolly(targetCamera, aznumeric_cast<float>(cursorDelta.m_y) * m_motionSpeedFn());
return OrbitDolly(targetCamera, aznumeric_cast<float>(cursorDelta.m_y) * m_motionSpeedFn());
}
void PivotDollyMotionCameraInput::SetDollyInputChannelId(const InputChannelId& dollyChannelId)
void OrbitDollyMotionCameraInput::SetDollyInputChannelId(const InputChannelId& dollyChannelId)
{
m_dollyChannelId = dollyChannelId;
}
ScrollTranslationCameraInput::ScrollTranslationCameraInput()
LookScrollTranslationCameraInput::LookScrollTranslationCameraInput()
{
m_scrollSpeedFn = []() constexpr
{
@ -686,7 +695,7 @@ namespace AzFramework
};
}
bool ScrollTranslationCameraInput::HandleEvents(
bool LookScrollTranslationCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
{
if (const auto* scroll = AZStd::get_if<ScrollEvent>(&event))
@ -697,7 +706,7 @@ namespace AzFramework
return !Idle();
}
Camera ScrollTranslationCameraInput::StepCamera(
Camera LookScrollTranslationCameraInput::StepCamera(
const Camera& targetCamera,
[[maybe_unused]] const ScreenVector& cursorDelta,
const float scrollDelta,
@ -771,6 +780,73 @@ namespace AzFramework
return camera;
}
FocusCameraInput::FocusCameraInput(const InputChannelId& focusChannelId, FocusOffsetFn offsetFn)
: m_focusChannelId(focusChannelId)
, m_offsetFn(offsetFn)
{
}
bool FocusCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
if (const auto* input = AZStd::get_if<DiscreteInputEvent>(&event))
{
if (input->m_channelId == m_focusChannelId && input->m_state == InputChannel::State::Began)
{
BeginActivation();
}
}
return !Idle();
}
Camera FocusCameraInput::StepCamera(
const Camera& targetCamera,
[[maybe_unused]] const ScreenVector& cursorDelta,
[[maybe_unused]] float scrollDelta,
[[maybe_unused]] float deltaTime)
{
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()));
const auto angles =
EulerAngles(AZ::Matrix3x3::CreateFromMatrix3x4(AZ::Matrix3x4::CreateLookAt(targetCamera.Translation(), m_pivotFn())));
m_nextCamera.m_pitch = angles.GetX();
m_nextCamera.m_yaw = angles.GetZ();
m_nextCamera.m_pivot = targetCamera.m_pivot;
}
// end the behavior when the camera is in alignment
if (AZ::IsCloseMag(targetCamera.m_pitch, m_nextCamera.m_pitch) && AZ::IsCloseMag(targetCamera.m_yaw, m_nextCamera.m_yaw))
{
EndActivation();
}
return m_nextCamera;
}
void FocusCameraInput::SetPivotFn(PivotFn pivotFn)
{
m_pivotFn = AZStd::move(pivotFn);
}
void FocusCameraInput::SetFocusInputChannelId(const InputChannelId& focusChannelId)
{
m_focusChannelId = focusChannelId;
}
bool CustomCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, const float scrollDelta)
{
return m_handleEventsFn(*this, event, cursorDelta, scrollDelta);
}
Camera CustomCameraInput::StepCamera(
const Camera& targetCamera, const ScreenVector& cursorDelta, const float scrollDelta, const float deltaTime)
{
return m_stepCameraFn(*this, targetCamera, cursorDelta, scrollDelta, deltaTime);
}
InputEvent BuildInputEvent(const InputChannel& inputChannel, const WindowSize& windowSize)
{
const auto& inputChannelId = inputChannel.GetInputChannelId();

@ -30,8 +30,11 @@ namespace AzFramework
AZ::Vector3 EulerAngles(const AZ::Matrix3x3& orientation);
//! A simple camera representation using spherical coordinates as input (pitch, yaw, pivot and offset).
//! The cameras transform and view can be obtained through accessor functions that use the internal
//! The camera's transform and view can be obtained through accessor functions that use the internal
//! spherical coordinates to calculate the position and orientation.
//! @note Modifying m_pivot directly and leaving m_offset as zero will produce a free look camera effect, giving
//! m_offset a value (e.g. in negative Y only) will produce an orbit camera effect, modifying X and Z of m_offset
//! will further alter the camera translation in relation to m_pivot so it appears off center.
struct Camera
{
AZ::Vector3 m_pivot = AZ::Vector3::CreateZero(); //!< Pivot point to rotate about (modified in world space).
@ -291,7 +294,7 @@ namespace AzFramework
Cameras m_cameras; //!< Represents a collection of camera inputs that together provide a camera controller.
private:
ScreenVector m_motionDelta; //!< The delta used for look/pivot/pan (rotation + translation) - two dimensional.
ScreenVector m_motionDelta; //!< The delta used for look/orbit/pan (rotation + translation) - two dimensional.
CursorState m_cursorState; //!< The current and previous position of the cursor (used to calculate movement delta).
float m_scrollDelta = 0.0f; //!< The delta used for dolly/movement (translation) - one dimensional.
bool m_handlingEvents = false; //!< Is the camera system currently handling events (events are consumed and not propagated).
@ -316,7 +319,7 @@ namespace AzFramework
return AZStd::fmod(yaw + AZ::Constants::TwoPi, AZ::Constants::TwoPi);
}
//! A camera input to handle motion deltas that can rotate or pivot the camera.
//! A camera input to handle motion deltas that can change the orientation of the camera (update pitch and yaw).
class RotateCameraInput : public CameraInput
{
public:
@ -348,15 +351,16 @@ namespace AzFramework
//! PanAxes build function that will return a pair of pan axes depending on the camera orientation.
using PanAxesFn = AZStd::function<PanAxes(const Camera& camera)>;
//! PanAxes to use while in 'look' camera behavior (free look).
//! PanAxes to use while in 'look' or 'orbit' camera behavior.
inline PanAxes LookPan(const Camera& camera)
{
const AZ::Matrix3x3 orientation = camera.Rotation();
return { orientation.GetBasisX(), orientation.GetBasisZ() };
}
//! PanAxes to use while in 'pivot' camera behavior.
inline PanAxes PivotPan(const Camera& camera)
//! Optional PanAxes to use while in 'orbit' camera behavior.
//! @note This will move the camera in the local X/Y plane instead of usual X/Z plane.
inline PanAxes OrbitPan(const Camera& camera)
{
const AZ::Matrix3x3 orientation = camera.Rotation();
@ -370,14 +374,23 @@ namespace AzFramework
return { basisX, basisY };
}
//! TranslationDeltaFn is used by PanCameraInput and TranslateCameraInput
//! @note Choose the appropriate function if the behavior should be operating as a free look camera (TranslatePivotLook)
//! or an orbit camera (TranslateOffsetOrbit).
using TranslationDeltaFn = AZStd::function<void(Camera& camera, const AZ::Vector3& delta)>;
inline void TranslatePivot(Camera& camera, const AZ::Vector3& delta)
//! Update the pivot camera position.
//! @note delta will need to have been transformed to world space, e.g. To move the camera right, (1, 0, 0) must
//! first be transformed by the orientation of the camera before being applied to m_pivot.
inline void TranslatePivotLook(Camera& camera, const AZ::Vector3& delta)
{
camera.m_pivot += delta;
}
inline void TranslateOffset(Camera& camera, const AZ::Vector3& delta)
//! Update the offset camera position.
//! @note delta still needs to be transformed to world space (as with TranslatePivotLook) but internally this is undone
//! to be performed in local space when being applied to m_offset.
inline void TranslateOffsetOrbit(Camera& camera, const AZ::Vector3& delta)
{
camera.m_offset += camera.View().TransformVector(delta);
}
@ -409,7 +422,7 @@ namespace AzFramework
//! Axes to use while translating the camera.
using TranslationAxesFn = AZStd::function<AZ::Matrix3x3(const Camera& camera)>;
//! TranslationAxes to use while in 'look' camera behavior (free look).
//! TranslationAxes to use while in 'look' or 'orbit' camera behavior.
inline AZ::Matrix3x3 LookTranslation(const Camera& camera)
{
const AZ::Matrix3x3 orientation = camera.Rotation();
@ -421,8 +434,8 @@ namespace AzFramework
return AZ::Matrix3x3::CreateFromColumns(basisX, basisY, basisZ);
}
//! TranslationAxes to use while in 'pivot' camera behavior.
inline AZ::Matrix3x3 PivotTranslation(const Camera& camera)
//! Optional TranslationAxes to use while in 'orbit' camera behavior.
inline AZ::Matrix3x3 OrbitTranslation(const Camera& camera)
{
const AZ::Matrix3x3 orientation = camera.Rotation();
@ -535,11 +548,11 @@ namespace AzFramework
bool m_boost = false; //!< Is the translation speed currently being multiplied/scaled upwards.
};
//! A camera input to handle discrete scroll events that can modify the camera pivot distance.
class PivotDollyScrollCameraInput : public CameraInput
//! A camera input to handle discrete scroll events that can modify the camera offset.
class OrbitDollyScrollCameraInput : public CameraInput
{
public:
PivotDollyScrollCameraInput();
OrbitDollyScrollCameraInput();
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
@ -548,11 +561,11 @@ namespace AzFramework
AZStd::function<float()> m_scrollSpeedFn;
};
//! A camera input to handle motion deltas that can modify the camera pivot distance.
class PivotDollyMotionCameraInput : public CameraInput
//! A camera input to handle motion deltas that can modify the camera offset.
class OrbitDollyMotionCameraInput : public CameraInput
{
public:
explicit PivotDollyMotionCameraInput(const InputChannelId& dollyChannelId);
explicit OrbitDollyMotionCameraInput(const InputChannelId& dollyChannelId);
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
@ -569,10 +582,10 @@ namespace AzFramework
};
//! A camera input to handle discrete scroll events that can scroll (translate) the camera along its forward axis.
class ScrollTranslationCameraInput : public CameraInput
class LookScrollTranslationCameraInput : public CameraInput
{
public:
ScrollTranslationCameraInput();
LookScrollTranslationCameraInput();
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
@ -583,40 +596,96 @@ namespace AzFramework
//! A camera input that doubles as its own set of camera inputs.
//! It is 'exclusive', so does not overlap with other sibling camera inputs - it runs its own set of camera inputs as 'children'.
class PivotCameraInput : public CameraInput
class OrbitCameraInput : public CameraInput
{
public:
using PivotFn = AZStd::function<AZ::Vector3(const AZ::Vector3& position, const AZ::Vector3& direction)>;
explicit PivotCameraInput(const InputChannelId& pivotChannelId);
explicit OrbitCameraInput(const InputChannelId& orbitChannelId);
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
bool Exclusive() const override;
void SetPivotInputChannelId(const InputChannelId& pivotChanneId);
void SetOrbitInputChannelId(const InputChannelId& orbitChanneId);
Cameras m_pivotCameras; //!< The camera inputs to run when this camera input is active (only these will run as it is exclusive).
Cameras m_orbitCameras; //!< The camera inputs to run when this camera input is active (only these will run as it is exclusive).
//! Override the default behavior for how a pivot point is calculated.
void SetPivotFn(PivotFn pivotFn);
private:
InputChannelId m_pivotChannelId; //!< Input channel to begin the pivot camera input.
PivotFn m_pivotFn; //!< The pivot position to use for this pivot camera (how is the pivot point calculated/retrieved).
InputChannelId m_orbitChannelId; //!< Input channel to begin the orbit camera input.
PivotFn m_pivotFn; //!< The pivot position to use for this orbit camera (how is the pivot point calculated/retrieved).
};
inline void PivotCameraInput::SetPivotFn(PivotFn pivotFn)
inline void OrbitCameraInput::SetPivotFn(PivotFn pivotFn)
{
m_pivotFn = AZStd::move(pivotFn);
}
inline bool PivotCameraInput::Exclusive() const
inline bool OrbitCameraInput::Exclusive() const
{
return true;
}
//! Callback to use for FocusCameraInput when a free look camera is being used.
//! @note This is when offset is zero.
inline AZ::Vector3 FocusLook(float)
{
return AZ::Vector3::CreateZero();
}
//! Callback to use for FocusCameraInput when a orbit camera is being used.
//! @note This is when offset is non zero.
inline AZ::Vector3 FocusOrbit(const float length)
{
return AZ::Vector3::CreateAxisY(-length);
}
using FocusOffsetFn = AZStd::function<AZ::Vector3(float)>;
//! A focus behavior to align the camera view to the position returned by the pivot function.
//! @note This only alters the camera orientation, the translation is unaffected.
class FocusCameraInput : public CameraInput
{
public:
using PivotFn = AZStd::function<AZ::Vector3()>;
FocusCameraInput(const InputChannelId& focusChannelId, FocusOffsetFn offsetFn);
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
//! Override the default behavior for how a pivot point is calculated.
void SetPivotFn(PivotFn pivotFn);
void SetFocusInputChannelId(const InputChannelId& focusChannelId);
private:
InputChannelId m_focusChannelId; //!< Input channel to begin the focus camera input.
Camera m_nextCamera;
PivotFn m_pivotFn;
FocusOffsetFn m_offsetFn;
};
//! Provides a CameraInput type that can be implemented without needing to create a new type deriving from CameraInput.
//! This can be very useful for specific use cases that are less generally applicable.
class CustomCameraInput : public CameraInput
{
public:
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
//! HandleEvents delegates directly to m_handleEventsFn.
AZStd::function<bool(CameraInput&, const InputEvent&, const ScreenVector&, float)> m_handleEventsFn;
//! StepCamera delegates directly to m_stepCameraFn.
AZStd::function<Camera(CameraInput&, const Camera&, const ScreenVector&, float, float)> m_stepCameraFn;
};
//! Map from a generic InputChannel event to a camera specific InputEvent.
InputEvent BuildInputEvent(const InputChannel& inputChannel, const WindowSize& windowSize);
} // namespace AzFramework

@ -167,6 +167,10 @@ set(FILES
Logging/MissingAssetLogger.cpp
Logging/MissingAssetLogger.h
Logging/MissingAssetNotificationBus.h
Matchmaking/IMatchmakingRequests.h
Matchmaking/MatchmakingRequests.cpp
Matchmaking/MatchmakingRequests.h
Matchmaking/MatchmakingNotifications.h
Scene/Scene.h
Scene/Scene.inl
Scene/Scene.cpp
@ -181,8 +185,9 @@ set(FILES
Script/ScriptRemoteDebugging.cpp
Script/ScriptRemoteDebugging.h
Session/ISessionHandlingRequests.h
Session/ISessionRequests.cpp
Session/ISessionRequests.h
Session/SessionRequests.cpp
Session/SessionRequests.h
Session/SessionConfig.cpp
Session/SessionConfig.h
Session/SessionNotifications.h

@ -20,6 +20,15 @@
namespace AzFramework
{
// xcb-xkb does not provide a generic event type, so we define our own.
// These fields are enough to get to the xkbType field, which can then be
// read to typecast the event to the right concrete type.
struct XcbXkbGenericEventT
{
uint8_t response_type;
uint8_t xkbType;
};
XcbInputDeviceKeyboard::XcbInputDeviceKeyboard(InputDeviceKeyboard& inputDevice)
: InputDeviceKeyboard::Implementation(inputDevice)
{
@ -39,19 +48,22 @@ namespace AzFramework
return;
}
XcbStdFreePtr<xcb_xkb_use_extension_reply_t> xkbUseExtensionReply{
xcb_xkb_use_extension_reply(connection, xcb_xkb_use_extension(connection, 1, 0), nullptr)
};
if (!xkbUseExtensionReply)
int initializeXkbExtensionSuccess = xkb_x11_setup_xkb_extension(
connection,
1,
0,
XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS,
nullptr,
nullptr,
&m_xkbEventCode,
nullptr
);
if (!initializeXkbExtensionSuccess)
{
AZ_Warning("ApplicationLinux", false, "Failed to initialize the xkb extension");
return;
}
if (!xkbUseExtensionReply->supported)
{
AZ_Warning("ApplicationLinux", false, "The X server does not support the xkb extension");
return;
}
m_coreDeviceId = xkb_x11_get_core_keyboard_device_id(connection);
@ -59,6 +71,43 @@ namespace AzFramework
m_xkbKeymap.reset(xkb_x11_keymap_new_from_device(m_xkbContext.get(), connection, m_coreDeviceId, XKB_KEYMAP_COMPILE_NO_FLAGS));
m_xkbState.reset(xkb_x11_state_new_from_device(m_xkbKeymap.get(), connection, m_coreDeviceId));
const uint16_t affectMap =
XCB_XKB_MAP_PART_KEY_TYPES
| XCB_XKB_MAP_PART_KEY_SYMS
| XCB_XKB_MAP_PART_MODIFIER_MAP
| XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS
| XCB_XKB_MAP_PART_KEY_ACTIONS
| XCB_XKB_MAP_PART_KEY_BEHAVIORS
| XCB_XKB_MAP_PART_VIRTUAL_MODS
| XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP
;
const uint16_t selectedEvents =
XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY
| XCB_XKB_EVENT_TYPE_MAP_NOTIFY
| XCB_XKB_EVENT_TYPE_STATE_NOTIFY
;
XcbStdFreePtr<xcb_generic_error_t> error{xcb_request_check(
connection,
xcb_xkb_select_events(
connection,
/* deviceSpec = */ XCB_XKB_ID_USE_CORE_KBD,
/* affectWhich = */ selectedEvents,
/* clear = */ 0,
/* selectAll = */ selectedEvents,
/* affectMap = */ affectMap,
/* map = */ affectMap,
/* details = */ nullptr
)
)};
if (error)
{
AZ_Warning("ApplicationLinux", false, "failed to select notify events from XKB");
return;
}
m_initialized = true;
}
@ -70,15 +119,17 @@ namespace AzFramework
bool XcbInputDeviceKeyboard::HasTextEntryStarted() const
{
return false;
return m_hasTextEntryStarted;
}
void XcbInputDeviceKeyboard::TextEntryStart(const InputDeviceKeyboard::VirtualKeyboardOptions& options)
{
m_hasTextEntryStarted = true;
}
void XcbInputDeviceKeyboard::TextEntryStop()
{
m_hasTextEntryStarted = false;
}
void XcbInputDeviceKeyboard::TickInputDevice()
@ -93,30 +144,45 @@ namespace AzFramework
return;
}
switch (event->response_type & ~0x80)
{
case XCB_KEY_PRESS:
const auto responseType = event->response_type & ~0x80;
if (responseType == XCB_KEY_PRESS)
{
auto* keyPress = reinterpret_cast<xcb_key_press_event_t*>(event);
const auto* keyPress = reinterpret_cast<xcb_key_press_event_t*>(event);
{
auto text = TextFromKeycode(m_xkbState.get(), keyPress->detail);
if (!text.empty())
{
QueueRawTextEvent(AZStd::move(text));
}
}
const InputChannelId* key = InputChannelFromKeyEvent(keyPress->detail);
if (key)
if (const InputChannelId* key = InputChannelFromKeyEvent(keyPress->detail))
{
QueueRawKeyEvent(*key, true);
}
break;
}
case XCB_KEY_RELEASE:
else if (responseType == XCB_KEY_RELEASE)
{
auto* keyRelease = reinterpret_cast<xcb_key_release_event_t*>(event);
const auto* keyRelease = reinterpret_cast<xcb_key_release_event_t*>(event);
const InputChannelId* key = InputChannelFromKeyEvent(keyRelease->detail);
if (key)
{
QueueRawKeyEvent(*key, false);
}
break;
}
else if (responseType == m_xkbEventCode)
{
const auto* xkbEvent = reinterpret_cast<XcbXkbGenericEventT*>(event);
switch (xkbEvent->xkbType)
{
case XCB_XKB_STATE_NOTIFY:
{
const auto* stateNotifyEvent = reinterpret_cast<xcb_xkb_state_notify_event_t*>(event);
UpdateState(stateNotifyEvent);
break;
}
}
}
}
@ -268,4 +334,34 @@ namespace AzFramework
default: return nullptr;
}
}
AZStd::string XcbInputDeviceKeyboard::TextFromKeycode(xkb_state* state, xkb_keycode_t code)
{
// Find out how much of a buffer we need
const size_t size = xkb_state_key_get_utf8(state, code, nullptr, 0);
if (!size)
{
return {};
}
// xkb_state_key_get_utf8 will null-terminate the resulting string, and
// will truncate the result to `size - 1` if there is not enough space
// for the null byte. The first call returns the size of the resulting
// string without including the null byte. AZStd::string internally
// includes space for the null byte, but that is not included in its
// `size()`. xkb_state_key_get_utf8 will always set `buf[size - 1] =
// 0`, so add 1 to `chars.size()` to include that internal null byte in
// the string.
AZStd::string chars;
chars.resize_no_construct(size);
xkb_state_key_get_utf8(state, code, chars.data(), chars.size() + 1);
return chars;
}
void XcbInputDeviceKeyboard::UpdateState(const xcb_xkb_state_notify_event_t* state)
{
if (m_initialized)
{
xkb_state_update_mask(m_xkbState.get(), state->baseMods, state->latchedMods, state->lockedMods, state->baseGroup, state->latchedGroup, state->lockedGroup);
}
}
} // namespace AzFramework

@ -13,6 +13,8 @@
#include <xcb/xcb.h>
#include <xkbcommon/xkbcommon.h>
struct xcb_xkb_state_notify_event_t;
namespace AzFramework
{
class XcbInputDeviceKeyboard
@ -37,10 +39,16 @@ namespace AzFramework
private:
[[nodiscard]] const InputChannelId* InputChannelFromKeyEvent(xcb_keycode_t code) const;
static AZStd::string TextFromKeycode(xkb_state* state, xkb_keycode_t code);
void UpdateState(const xcb_xkb_state_notify_event_t* state);
XcbUniquePtr<xkb_context, xkb_context_unref> m_xkbContext;
XcbUniquePtr<xkb_keymap, xkb_keymap_unref> m_xkbKeymap;
XcbUniquePtr<xkb_state, xkb_state_unref> m_xkbState;
int m_coreDeviceId{-1};
uint8_t m_xkbEventCode{0};
bool m_initialized{false};
bool m_hasTextEntryStarted{false};
};
} // namespace AzFramework

@ -53,31 +53,31 @@ namespace UnitTest
};
m_firstPersonTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
m_translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslatePivot);
m_translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslatePivotLook);
m_pivotCamera = AZStd::make_shared<AzFramework::PivotCameraInput>(m_pivotChannelId);
m_pivotCamera->SetPivotFn(
m_orbitCamera = AZStd::make_shared<AzFramework::OrbitCameraInput>(m_orbitChannelId);
m_orbitCamera->SetPivotFn(
[this](const AZ::Vector3&, const AZ::Vector3&)
{
return m_pivot;
});
auto pivotRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(AzFramework::InputDeviceMouse::Button::Left);
auto orbitRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(AzFramework::InputDeviceMouse::Button::Left);
// set rotate speed to be a value that will scale motion delta (pixels moved) by a thousandth.
pivotRotateCamera->m_rotateSpeedFn = []()
orbitRotateCamera->m_rotateSpeedFn = []()
{
return 0.001f;
};
auto pivotTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
m_translateCameraInputChannelIds, AzFramework::PivotTranslation, AzFramework::TranslateOffset);
auto orbitTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
m_translateCameraInputChannelIds, AzFramework::OrbitTranslation, AzFramework::TranslateOffsetOrbit);
m_pivotCamera->m_pivotCameras.AddCamera(pivotRotateCamera);
m_pivotCamera->m_pivotCameras.AddCamera(pivotTranslateCamera);
m_orbitCamera->m_orbitCameras.AddCamera(orbitRotateCamera);
m_orbitCamera->m_orbitCameras.AddCamera(orbitTranslateCamera);
m_cameraSystem->m_cameras.AddCamera(m_firstPersonRotateCamera);
m_cameraSystem->m_cameras.AddCamera(m_firstPersonTranslateCamera);
m_cameraSystem->m_cameras.AddCamera(m_pivotCamera);
m_cameraSystem->m_cameras.AddCamera(m_orbitCamera);
// these tests rely on using motion delta, not cursor positions (default is true)
AzFramework::ed_cameraSystemUseCursor = false;
@ -87,7 +87,7 @@ namespace UnitTest
{
AzFramework::ed_cameraSystemUseCursor = true;
m_pivotCamera.reset();
m_orbitCamera.reset();
m_firstPersonRotateCamera.reset();
m_firstPersonTranslateCamera.reset();
@ -97,11 +97,11 @@ namespace UnitTest
AllocatorsTestFixture::TearDown();
}
AzFramework::InputChannelId m_pivotChannelId = AzFramework::InputChannelId("keyboard_key_modifier_alt_l");
AzFramework::InputChannelId m_orbitChannelId = AzFramework::InputChannelId("keyboard_key_modifier_alt_l");
AzFramework::TranslateCameraInputChannelIds m_translateCameraInputChannelIds;
AZStd::shared_ptr<AzFramework::RotateCameraInput> m_firstPersonRotateCamera;
AZStd::shared_ptr<AzFramework::TranslateCameraInput> m_firstPersonTranslateCamera;
AZStd::shared_ptr<AzFramework::PivotCameraInput> m_pivotCamera;
AZStd::shared_ptr<AzFramework::OrbitCameraInput> m_orbitCamera;
AZ::Vector3 m_pivot = AZ::Vector3::CreateZero();
//! This is approximately Pi/2 * 1000 - this can be used to rotate the camera 90 degrees (pitch or yaw based
@ -109,17 +109,17 @@ namespace UnitTest
inline static const int PixelMotionDelta = 1570;
};
TEST_F(CameraInputFixture, BeginAndEndPivotCameraInputConsumesCorrectEvents)
TEST_F(CameraInputFixture, BeginAndEndOrbitCameraInputConsumesCorrectEvents)
{
// begin pivot camera
// begin orbit camera
const bool consumed1 = HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceKeyboard::Key::ModifierAltL,
AzFramework::InputChannel::State::Began });
// begin listening for pivot rotate (click detector) - event is not consumed
// begin listening for orbit rotate (click detector) - event is not consumed
const bool consumed2 = HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
// begin pivot rotate (mouse has moved sufficient distance to initiate)
// begin orbit rotate (mouse has moved sufficient distance to initiate)
const bool consumed3 = HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ 5 });
// end pivot (mouse up) - event is not consumed
// end orbit (mouse up) - event is not consumed
const bool consumed4 = HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Ended });
@ -260,10 +260,10 @@ namespace UnitTest
EXPECT_TRUE(activationEnded);
}
TEST_F(CameraInputFixture, PivotCameraInputHandlesLookAtPointAndSelfAtSamePositionWhenPivoting)
TEST_F(CameraInputFixture, OrbitCameraInputHandlesLookAtPointAndSelfAtSamePositionWhenOrbiting)
{
// create pathological lookAtFn that just returns the same position as the camera
m_pivotCamera->SetPivotFn(
m_orbitCamera->SetPivotFn(
[](const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
{
return position;
@ -275,7 +275,7 @@ namespace UnitTest
AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), expectedCameraPosition));
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_pivotChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
// verify the camera yaw has not changed and pivot point matches the expected camera position
using ::testing::FloatNear;
@ -321,14 +321,14 @@ namespace UnitTest
EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3::CreateZero()));
}
TEST_F(CameraInputFixture, PivotRotateCameraInputRotatesPitchOffsetByNinetyDegreesWithRequiredPixelDelta)
TEST_F(CameraInputFixture, OrbitRotateCameraInputRotatesPitchOffsetByNinetyDegreesWithRequiredPixelDelta)
{
const auto cameraStartingPosition = AZ::Vector3::CreateAxisY(-20.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
m_pivot = AZ::Vector3::CreateAxisY(-10.0f);
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_pivotChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta });
@ -344,14 +344,14 @@ namespace UnitTest
EXPECT_THAT(m_camera.Translation(), IsCloseTolerance(expectedCameraEndingPosition, 0.01f));
}
TEST_F(CameraInputFixture, PivotRotateCameraInputRotatesYawOffsetByNinetyDegreesWithRequiredPixelDelta)
TEST_F(CameraInputFixture, OrbitRotateCameraInputRotatesYawOffsetByNinetyDegreesWithRequiredPixelDelta)
{
const auto cameraStartingPosition = AZ::Vector3(15.0f, -20.0f, 0.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
m_pivot = AZ::Vector3(10.0f, -10.0f, 0.0f);
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_pivotChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ -PixelMotionDelta });

@ -0,0 +1,17 @@
/*
* 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 <gmock/gmock.h>
#include <AzCore/std/string/string.h>
inline testing::PolymorphicMatcher<testing::internal::StrEqualityMatcher<AZStd::string>> StrEq(const AZStd::string& str)
{
return ::testing::MakePolymorphicMatcher(testing::internal::StrEqualityMatcher<AZStd::string>(str, true, true));
}

@ -28,6 +28,10 @@ xcb_generic_event_t* xcb_poll_for_event(xcb_connection_t* c)
{
return MockXcbInterface::Instance()->xcb_poll_for_event(c);
}
xcb_generic_error_t* xcb_request_check(xcb_connection_t* c, xcb_void_cookie_t cookie)
{
return MockXcbInterface::Instance()->xcb_request_check(c, cookie);
}
// ----------------------------------------------------------------------------
// xcb-xkb
@ -39,6 +43,10 @@ xcb_xkb_use_extension_reply_t* xcb_xkb_use_extension_reply(xcb_connection_t* c,
{
return MockXcbInterface::Instance()->xcb_xkb_use_extension_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_xkb_select_events(xcb_connection_t* c, xcb_xkb_device_spec_t deviceSpec, uint16_t affectWhich, uint16_t clear, uint16_t selectAll, uint16_t affectMap, uint16_t map, const void* details)
{
return MockXcbInterface::Instance()->xcb_xkb_select_events(c, deviceSpec, affectWhich, clear, selectAll, affectMap, map, details);
}
// ----------------------------------------------------------------------------
// xkb-x11
@ -46,7 +54,7 @@ int32_t xkb_x11_get_core_keyboard_device_id(xcb_connection_t* connection)
{
return MockXcbInterface::Instance()->xkb_x11_get_core_keyboard_device_id(connection);
}
struct xkb_keymap* xkb_x11_keymap_new_from_device(struct xkb_context* context, xcb_connection_t* connection, int32_t device_id, enum xkb_keymap_compile_flags flags)
xkb_keymap* xkb_x11_keymap_new_from_device(xkb_context* context, xcb_connection_t* connection, int32_t device_id, xkb_keymap_compile_flags flags)
{
return MockXcbInterface::Instance()->xkb_x11_keymap_new_from_device(context, connection, device_id, flags);
}
@ -54,28 +62,58 @@ xkb_state* xkb_x11_state_new_from_device(xkb_keymap* keymap, xcb_connection_t* c
{
return MockXcbInterface::Instance()->xkb_x11_state_new_from_device(keymap, connection, device_id);
}
int xkb_x11_setup_xkb_extension(
xcb_connection_t* connection,
uint16_t major_xkb_version,
uint16_t minor_xkb_version,
xkb_x11_setup_xkb_extension_flags flags,
uint16_t* major_xkb_version_out,
uint16_t* minor_xkb_version_out,
uint8_t* base_event_out,
uint8_t* base_error_out)
{
return MockXcbInterface::Instance()->xkb_x11_setup_xkb_extension(
connection, major_xkb_version, minor_xkb_version, flags, major_xkb_version_out, minor_xkb_version_out, base_event_out,
base_error_out);
}
// ----------------------------------------------------------------------------
// xkbcommon
xkb_context* xkb_context_new(enum xkb_context_flags flags)
xkb_context* xkb_context_new(xkb_context_flags flags)
{
return MockXcbInterface::Instance()->xkb_context_new(flags);
}
void xkb_context_unref(xkb_context *context)
void xkb_context_unref(xkb_context* context)
{
return MockXcbInterface::Instance()->xkb_context_unref(context);
}
void xkb_keymap_unref(xkb_keymap *keymap)
void xkb_keymap_unref(xkb_keymap* keymap)
{
return MockXcbInterface::Instance()->xkb_keymap_unref(keymap);
}
void xkb_state_unref(xkb_state *state)
void xkb_state_unref(xkb_state* state)
{
return MockXcbInterface::Instance()->xkb_state_unref(state);
}
xkb_keysym_t xkb_state_key_get_one_sym(xkb_state *state, xkb_keycode_t key)
xkb_keysym_t xkb_state_key_get_one_sym(xkb_state* state, xkb_keycode_t key)
{
return MockXcbInterface::Instance()->xkb_state_key_get_one_sym(state, key);
}
int xkb_state_key_get_utf8(xkb_state* state, xkb_keycode_t key, char* buffer, size_t size)
{
return MockXcbInterface::Instance()->xkb_state_key_get_utf8(state, key, buffer, size);
}
xkb_state_component xkb_state_update_mask(
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)
{
return MockXcbInterface::Instance()->xkb_state_update_mask(
state, depressed_mods, latched_mods, locked_mods, depressed_layout, latched_layout, locked_layout);
}
}

@ -17,6 +17,7 @@
#include <xcb/xkb.h>
#undef explicit
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-x11.h>
#include "Printers.h"
@ -35,6 +36,7 @@ struct xkb_keymap
struct xkb_state
{
xkb_mod_mask_t m_modifiers{};
};
class MockXcbInterface
@ -48,7 +50,7 @@ public:
MockXcbInterface(MockXcbInterface&&) = delete;
MockXcbInterface& operator=(const MockXcbInterface&) = delete;
MockXcbInterface& operator=(MockXcbInterface&&) = delete;
~MockXcbInterface()
virtual ~MockXcbInterface()
{
self = nullptr;
}
@ -59,22 +61,27 @@ public:
MOCK_CONST_METHOD2(xcb_connect, xcb_connection_t*(const char* displayname, int* screenp));
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));
// 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));
MOCK_CONST_METHOD3(xcb_xkb_use_extension_reply, xcb_xkb_use_extension_reply_t*(xcb_connection_t* c, xcb_xkb_use_extension_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD8(xcb_xkb_select_events, xcb_void_cookie_t(xcb_connection_t* c, xcb_xkb_device_spec_t deviceSpec, uint16_t affectWhich, uint16_t clear, uint16_t selectAll, uint16_t affectMap, uint16_t map, const void* details));
// xkb-x11
MOCK_CONST_METHOD1(xkb_x11_get_core_keyboard_device_id, int32_t(xcb_connection_t* connection));
MOCK_CONST_METHOD4(xkb_x11_keymap_new_from_device, xkb_keymap*(xkb_context* context, xcb_connection_t* connection, int32_t device_id, xkb_keymap_compile_flags flags));
MOCK_CONST_METHOD3(xkb_x11_state_new_from_device, xkb_state*(xkb_keymap* keymap, xcb_connection_t* connection, int32_t device_id));
MOCK_CONST_METHOD8(xkb_x11_setup_xkb_extension, int(xcb_connection_t* connection, uint16_t major_xkb_version, uint16_t minor_xkb_version, xkb_x11_setup_xkb_extension_flags flags, uint16_t* major_xkb_version_out, uint16_t* minor_xkb_version_out, uint8_t* base_event_out, uint8_t* base_error_out));
// xkbcommon
MOCK_CONST_METHOD1(xkb_context_new, xkb_context*(xkb_context_flags flags));
MOCK_CONST_METHOD1(xkb_context_unref, void(xkb_context* context));
MOCK_CONST_METHOD1(xkb_keymap_unref, void(xkb_keymap* keymap));
MOCK_CONST_METHOD1(xkb_state_unref, void(xkb_state* state));
MOCK_CONST_METHOD2(xkb_state_key_get_one_sym, xkb_keysym_t(xkb_state *state, xkb_keycode_t key));
MOCK_CONST_METHOD2(xkb_state_key_get_one_sym, xkb_keysym_t(xkb_state* state, xkb_keycode_t key));
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));
private:
static inline MockXcbInterface* self = nullptr;

@ -0,0 +1,25 @@
/*
* 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 "XcbBaseTestFixture.h"
namespace AzFramework
{
void XcbBaseTestFixture::SetUp()
{
using testing::Return;
using testing::_;
testing::Test::SetUp();
EXPECT_CALL(m_interface, xcb_connect(_, _))
.WillOnce(Return(&m_connection));
EXPECT_CALL(m_interface, xcb_disconnect(&m_connection))
.Times(1);
}
} // namespace AzFramework

@ -0,0 +1,29 @@
/*
* 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 <gtest/gtest.h>
#include <xcb/xcb.h>
#include "MockXcbInterface.h"
namespace AzFramework
{
// Sets up mock behavior for the xcb library, providing an xcb_connection_t that is returned from a call to xcb_connect
class XcbBaseTestFixture
: public testing::Test
{
public:
void SetUp() override;
protected:
testing::NiceMock<MockXcbInterface> m_interface;
xcb_connection_t m_connection{};
};
} // namespace AzFramework

@ -6,6 +6,7 @@
*
*/
#include <gmock/gmock-actions.h>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
@ -13,8 +14,11 @@
#include <AzFramework/XcbApplication.h>
#include <AzFramework/XcbInputDeviceKeyboard.h>
#include <AzFramework/Input/Buses/Notifications/InputTextNotificationBus.h>
#include "MockXcbInterface.h"
#include "Matchers.h"
#include "Actions.h"
#include "XcbBaseTestFixture.h"
template<typename T>
xcb_generic_event_t MakeEvent(T event)
@ -24,26 +28,126 @@ xcb_generic_event_t MakeEvent(T event)
namespace AzFramework
{
TEST(XcbInputDeviceKeyboard, InputChannelsUpdateStateFromXcbEvents)
// Sets up default behavior for mock keyboard responses to xcb methods
class XcbInputDeviceKeyboardTests
: public XcbBaseTestFixture
{
using testing::Return;
using testing::Eq;
using testing::_;
MockXcbInterface interface;
void SetUp() override
{
using testing::Return;
using testing::SetArgPointee;
using testing::_;
XcbBaseTestFixture::SetUp();
EXPECT_CALL(m_interface, xkb_context_new(XKB_CONTEXT_NO_FLAGS))
.WillOnce(Return(&m_xkbContext));
EXPECT_CALL(m_interface, xkb_context_unref(&m_xkbContext))
.Times(1);
EXPECT_CALL(m_interface, xkb_x11_keymap_new_from_device(&m_xkbContext, &m_connection, s_coreDeviceId, XKB_KEYMAP_COMPILE_NO_FLAGS))
.WillOnce(Return(&m_xkbKeymap));
EXPECT_CALL(m_interface, xkb_keymap_unref(&m_xkbKeymap))
.Times(1);
EXPECT_CALL(m_interface, xkb_x11_state_new_from_device(&m_xkbKeymap, &m_connection, s_coreDeviceId))
.WillOnce(Return(&m_xkbState));
EXPECT_CALL(m_interface, xkb_state_unref(&m_xkbState))
.Times(1);
ON_CALL(m_interface, xkb_x11_setup_xkb_extension(&m_connection, 1, 0, XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, _, _, _, _))
.WillByDefault(DoAll(
SetArgPointee<6>(s_xkbEventCode), // Set the "base_event_out" argument to the xkbEventCode, the value to identify XKB events
Return(1)
));
constexpr unsigned int xcbXkbSelectEventsSequence = 342;
ON_CALL(m_interface, xcb_xkb_select_events(&m_connection, _, _, _, _, _, _, _))
.WillByDefault(Return(xcb_void_cookie_t{/*.sequence = */ xcbXkbSelectEventsSequence}));
ON_CALL(m_interface, xcb_request_check(&m_connection, testing::Field(&xcb_void_cookie_t::sequence, testing::Eq(xcbXkbSelectEventsSequence))))
.WillByDefault(Return(nullptr)); // indicates success
ON_CALL(m_interface, xkb_x11_get_core_keyboard_device_id(&m_connection))
.WillByDefault(Return(s_coreDeviceId));
xcb_connection_t connection{};
xkb_context xkbContext{};
xkb_keymap xkbKeymap{};
xkb_state xkbState{};
const int32_t coreDeviceId{1};
ON_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForAKey))
.WillByDefault(Return(XKB_KEY_a))
;
ON_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForShiftLKey))
.WillByDefault(Return(XKB_KEY_Shift_L))
;
ON_CALL(m_interface, xkb_state_update_mask(&m_xkbState, _, _, _, _, _, _))
.WillByDefault(testing::Invoke(this, &XcbInputDeviceKeyboardTests::UpdateStateMask));
ON_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForAKey, nullptr, 0))
.WillByDefault(Return(1));
ON_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithoutShift, s_keycodeForAKey, _, 2))
.WillByDefault(DoAll(
SetArgPointee<2>('a'),
Return(1)
));
ON_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithShift, s_keycodeForAKey, _, 2))
.WillByDefault(DoAll(
SetArgPointee<2>('A'),
Return(1)
));
ON_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForShiftLKey, nullptr, 0))
.WillByDefault(Return(0));
}
private:
xkb_state_component UpdateStateMask(
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)
{
state->m_modifiers = depressed_mods | locked_mods;
return {};
}
protected:
xkb_context m_xkbContext{};
xkb_keymap m_xkbKeymap{};
xkb_state m_xkbState{};
const testing::Matcher<xkb_state*> m_matchesStateWithoutShift = testing::AllOf(&m_xkbState, testing::Field(&xkb_state::m_modifiers, 0));
const testing::Matcher<xkb_state*> m_matchesStateWithShift = testing::AllOf(&m_xkbState, testing::Field(&xkb_state::m_modifiers, XCB_MOD_MASK_SHIFT));
static constexpr int32_t s_coreDeviceId{1};
static constexpr uint8_t s_xkbEventCode{85};
static constexpr xcb_keycode_t s_keycodeForAKey{38};
static constexpr xcb_keycode_t s_keycodeForShiftLKey{50};
};
class InputTextNotificationListener
: public InputTextNotificationBus::Handler
{
public:
InputTextNotificationListener()
{
BusConnect();
}
MOCK_METHOD2(OnInputTextEvent, void(const AZStd::string& /*textUTF8*/, bool& /*o_hasBeenConsumed*/));
};
constexpr xcb_keycode_t keycodeForAKey = 38;
TEST_F(XcbInputDeviceKeyboardTests, InputChannelsUpdateStateFromXcbEvents)
{
using testing::DoAll;
using testing::Eq;
using testing::Return;
using testing::SetArgPointee;
using testing::_;
const AZStd::array events
{
MakeEvent(xcb_key_press_event_t{
/*.response_type = */ XCB_KEY_PRESS,
/*.detail = */ keycodeForAKey,
/*.detail = */ s_keycodeForAKey,
/*.sequence = */ 0,
/*.time = */ 0,
/*.root = */ 0,
@ -59,7 +163,7 @@ namespace AzFramework
}),
MakeEvent(xcb_key_release_event_t{
/*.response_type = */ XCB_KEY_RELEASE,
/*.detail = */ keycodeForAKey,
/*.detail = */ s_keycodeForAKey,
/*.sequence = */ 0,
/*.time = */ 0,
/*.root = */ 0,
@ -75,51 +179,20 @@ namespace AzFramework
}),
};
EXPECT_CALL(interface, xcb_connect(_, _))
.WillOnce(Return(&connection));
EXPECT_CALL(interface, xcb_disconnect(&connection))
.Times(1);
EXPECT_CALL(interface, xkb_context_new(XKB_CONTEXT_NO_FLAGS))
.WillOnce(Return(&xkbContext));
EXPECT_CALL(interface, xkb_context_unref(&xkbContext))
.Times(1);
EXPECT_CALL(interface, xkb_x11_keymap_new_from_device(&xkbContext, &connection, coreDeviceId, XKB_KEYMAP_COMPILE_NO_FLAGS))
.WillOnce(Return(&xkbKeymap));
EXPECT_CALL(interface, xkb_keymap_unref(&xkbKeymap))
.Times(1);
EXPECT_CALL(interface, xkb_x11_state_new_from_device(&xkbKeymap, &connection, coreDeviceId))
.WillOnce(Return(&xkbState));
EXPECT_CALL(interface, xkb_state_unref(&xkbState))
.Times(1);
EXPECT_CALL(interface, xcb_xkb_use_extension(&connection, 1, 0));
EXPECT_CALL(interface, xcb_xkb_use_extension_reply(&connection, _, _))
.WillOnce(ReturnMalloc<xcb_xkb_use_extension_reply_t>(
/* .response_type =*/static_cast<uint8_t>(XCB_XKB_USE_EXTENSION),
/* .supported =*/ static_cast<uint8_t>(1))
);
EXPECT_CALL(interface, xkb_x11_get_core_keyboard_device_id(&connection))
.WillRepeatedly(Return(coreDeviceId));
// 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 we malloc new copies
// here
EXPECT_CALL(interface, xcb_poll_for_event(&connection))
EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[0]))
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[1]))
.WillOnce(Return(nullptr))
;
EXPECT_CALL(interface, xkb_state_key_get_one_sym(&xkbState, keycodeForAKey))
.WillOnce(Return(XKB_KEY_a))
.WillOnce(Return(XKB_KEY_a))
;
EXPECT_CALL(m_interface, xkb_state_key_get_one_sym(&m_xkbState, s_keycodeForAKey))
.Times(2);
Application application;
application.Start({}, {});
@ -142,4 +215,219 @@ namespace AzFramework
application.Stop();
}
TEST_F(XcbInputDeviceKeyboardTests, TextEnteredFromXcbKeyPressEvents)
{
using testing::DoAll;
using testing::Eq;
using testing::Return;
using testing::SetArgPointee;
using testing::_;
// press a
// release a
// press shift
// press a
// release a
// release shift
const AZStd::array events
{
MakeEvent(xcb_key_press_event_t{
/*.response_type = */ XCB_KEY_PRESS,
/*.detail = */ s_keycodeForAKey,
/*.sequence = */ 0,
/*.time = */ 0,
/*.root = */ 0,
/*.event = */ 0,
/*.child = */ 0,
/*.root_x = */ 0,
/*.root_y = */ 0,
/*.event_x = */ 0,
/*.event_y = */ 0,
/*.state = */ 0,
/*.same_screen = */ 0,
/*.pad0 = */ 0
}),
MakeEvent(xcb_key_release_event_t{
/*.response_type = */ XCB_KEY_RELEASE,
/*.detail = */ s_keycodeForAKey,
/*.sequence = */ 0,
/*.time = */ 0,
/*.root = */ 0,
/*.event = */ 0,
/*.child = */ 0,
/*.root_x = */ 0,
/*.root_y = */ 0,
/*.event_x = */ 0,
/*.event_y = */ 0,
/*.state = */ 0,
/*.same_screen = */ 0,
/*.pad0 = */ 0
}),
// Pressing a modifier key will generate a key press event followed
// by a state notify event
MakeEvent(xcb_key_press_event_t{
/*.response_type = */ XCB_KEY_PRESS,
/*.detail = */ s_keycodeForShiftLKey,
/*.sequence = */ 0,
/*.time = */ 0,
/*.root = */ 0,
/*.event = */ 0,
/*.child = */ 0,
/*.root_x = */ 0,
/*.root_y = */ 0,
/*.event_x = */ 0,
/*.event_y = */ 0,
/*.state = */ 0,
/*.same_screen = */ 0,
/*.pad0 = */ 0
}),
MakeEvent(xcb_xkb_state_notify_event_t{
/*.response_type = */ s_xkbEventCode,
/*.xkbType = */ XCB_XKB_STATE_NOTIFY,
/*.sequence = */ 0,
/*.time = */ 0,
/*.deviceID = */ s_coreDeviceId,
/*.mods = */ XCB_MOD_MASK_SHIFT,
/*.baseMods = */ XCB_MOD_MASK_SHIFT,
/*.latchedMods = */ 0,
/*.lockedMods = */ 0,
/*.group = */ 0,
/*.baseGroup = */ 0,
/*.latchedGroup = */ 0,
/*.lockedGroup = */ 0,
/*.compatState = */ XCB_MOD_MASK_SHIFT,
/*.grabMods = */ XCB_MOD_MASK_SHIFT,
/*.compatGrabMods = */ XCB_MOD_MASK_SHIFT,
/*.lookupMods = */ XCB_MOD_MASK_SHIFT,
/*.compatLoockupMods = */ XCB_MOD_MASK_SHIFT,
/*.ptrBtnState = */ 0,
/*.changed = */ 0,
/*.keycode = */ s_keycodeForShiftLKey,
/*.eventType = */ XCB_KEY_PRESS,
/*.requestMajor = */ 0,
/*.requestMinor = */ 0,
}),
MakeEvent(xcb_key_press_event_t{
/*.response_type = */ XCB_KEY_PRESS,
/*.detail = */ s_keycodeForAKey,
/*.sequence = */ 0,
/*.time = */ 0,
/*.root = */ 0,
/*.event = */ 0,
/*.child = */ 0,
/*.root_x = */ 0,
/*.root_y = */ 0,
/*.event_x = */ 0,
/*.event_y = */ 0,
/*.state = */ 0,
/*.same_screen = */ 0,
/*.pad0 = */ 0
}),
MakeEvent(xcb_key_release_event_t{
/*.response_type = */ XCB_KEY_RELEASE,
/*.detail = */ s_keycodeForAKey,
/*.sequence = */ 0,
/*.time = */ 0,
/*.root = */ 0,
/*.event = */ 0,
/*.child = */ 0,
/*.root_x = */ 0,
/*.root_y = */ 0,
/*.event_x = */ 0,
/*.event_y = */ 0,
/*.state = */ 0,
/*.same_screen = */ 0,
/*.pad0 = */ 0
}),
MakeEvent(xcb_key_release_event_t{
/*.response_type = */ XCB_KEY_RELEASE,
/*.detail = */ s_keycodeForShiftLKey,
/*.sequence = */ 0,
/*.time = */ 0,
/*.root = */ 0,
/*.event = */ 0,
/*.child = */ 0,
/*.root_x = */ 0,
/*.root_y = */ 0,
/*.event_x = */ 0,
/*.event_y = */ 0,
/*.state = */ 0,
/*.same_screen = */ 0,
/*.pad0 = */ 0
}),
MakeEvent(xcb_xkb_state_notify_event_t{
/*.response_type = */ s_xkbEventCode,
/*.xkbType = */ XCB_XKB_STATE_NOTIFY,
/*.sequence = */ 0,
/*.time = */ 0,
/*.deviceID = */ s_coreDeviceId,
/*.mods = */ 0,
/*.baseMods = */ 0,
/*.latchedMods = */ 0,
/*.lockedMods = */ 0,
/*.group = */ 0,
/*.baseGroup = */ 0,
/*.latchedGroup = */ 0,
/*.lockedGroup = */ 0,
/*.compatState = */ 0,
/*.grabMods = */ 0,
/*.compatGrabMods = */ 0,
/*.lookupMods = */ 0,
/*.compatLoockupMods = */ 0,
/*.ptrBtnState = */ 0,
/*.changed = */ 0,
/*.keycode = */ s_keycodeForShiftLKey,
/*.eventType = */ XCB_KEY_RELEASE,
/*.requestMajor = */ 0,
/*.requestMinor = */ 0,
}),
};
// 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 we malloc new copies
// here
EXPECT_CALL(m_interface, xcb_poll_for_event(&m_connection))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[0])) // press a
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[1])) // release a
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[2])) // press shift
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[3])) // state notify shift is down
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[4])) // press a
.WillOnce(Return(nullptr))
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[5])) // release a
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[6])) // release shift
.WillOnce(ReturnMalloc<xcb_generic_event_t>(events[7])) // state notify shift is up
.WillRepeatedly(Return(nullptr))
;
EXPECT_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForAKey, nullptr, 0))
.Times(2);
EXPECT_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithoutShift, s_keycodeForAKey, _, 2))
.Times(1);
EXPECT_CALL(m_interface, xkb_state_key_get_utf8(m_matchesStateWithShift, s_keycodeForAKey, _, 2))
.Times(1);
EXPECT_CALL(m_interface, xkb_state_key_get_utf8(&m_xkbState, s_keycodeForShiftLKey, nullptr, 0))
.Times(1);
InputTextNotificationListener textListener;
EXPECT_CALL(textListener, OnInputTextEvent(StrEq("a"), _)).Times(1);
EXPECT_CALL(textListener, OnInputTextEvent(StrEq("A"), _)).Times(1);
Application application;
application.Start({}, {});
for (int i = 0; i < 4; ++i)
{
application.PumpSystemEventLoopUntilEmpty();
application.TickSystem();
application.Tick();
}
application.Stop();
}
} // namespace AzFramework

@ -9,9 +9,12 @@
set(FILES
Actions.h
Main.cpp
Matchers.h
MockXcbInterface.cpp
MockXcbInterface.h
Printers.cpp
Printers.h
XcbBaseTestFixture.cpp
XcbBaseTestFixture.h
XcbInputDeviceKeyboardTests.cpp
)

@ -6,10 +6,11 @@
*
*/
#include <AzQtComponents/AzQtComponents_Traits_Platform.h>
#include <AzQtComponents/Components/Widgets/FileDialog.h>
#include <QMessageBox>
#include <QRegExp>
#include <QRegularExpression>
namespace AzQtComponents
{
@ -24,7 +25,12 @@ namespace AzQtComponents
// Trigger Qt's save filename dialog
// If filePath isn't empty, it means we are prompting again because the filename was invalid,
// so pass it instead of the directory so the filename is pre-filled in for the user
filePath = QFileDialog::getSaveFileName(parent, caption, (filePath.isEmpty()) ? dir : filePath, filter, selectedFilter, options);
QString localSelectedFilter;
filePath = QFileDialog::getSaveFileName(parent, caption, (filePath.isEmpty()) ? dir : filePath, filter, &localSelectedFilter, options);
if (selectedFilter)
{
*selectedFilter = localSelectedFilter;
}
if (!filePath.isEmpty())
{
@ -32,15 +38,39 @@ namespace AzQtComponents
QString fileName = fileInfo.fileName();
// Check if the filename has any invalid characters
QRegExp validFileNameRegex("^[a-zA-Z0-9_\\-./]*$");
shouldPromptAgain = !validFileNameRegex.exactMatch(fileName);
QRegularExpression validFileNameRegex("^[a-zA-Z0-9_\\-./]*$");
QRegularExpressionMatch validFileNameMatch = validFileNameRegex.match(fileName);
// If the filename had invalid characters, then show a warning message and then we will re-prompt the save filename dialog
if (shouldPromptAgain)
if (!validFileNameMatch.hasMatch())
{
QMessageBox::warning(parent, QObject::tr("Invalid filename"),
QObject::tr("O3DE assets are restricted to alphanumeric characters, hyphens (-), underscores (_), and dots (.)\n\n%1").arg(fileName));
shouldPromptAgain = true;
continue;
}
else
{
shouldPromptAgain = false;
}
#if AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION
// If a filter was selected, then make sure that the resulting filename ends with that extension. On systems that use the default QFileDialog,
// the extension is not guaranteed to be set in the resulting filename
if (FileDialog::ApplyMissingExtension(localSelectedFilter, filePath))
{
// If an extension had to be applied, then the file dialog did not handle the case of overwriting existing files.
// We need to check that condition before we proceed
QFileInfo updatedFilePath(filePath);
if (updatedFilePath.exists())
{
QMessageBox::StandardButton overwriteSelection = QMessageBox::question(parent,
QObject::tr("File exists"),
QObject::tr("%1 exists. Do you want to overwrite the existing file?").arg(updatedFilePath.fileName()));
shouldPromptAgain = (overwriteSelection == QMessageBox::No);
}
}
#endif // AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION
}
else
{
@ -51,4 +81,56 @@ namespace AzQtComponents
return filePath;
}
bool FileDialog::ApplyMissingExtension(const QString& selectedFilter, QString& filePath)
{
if (selectedFilter.isEmpty())
{
return false;
}
// According to the QT documentation for QFileDialog, the selected filter will come in the form
// <Filter Name> (<filter pattern1> <filter pattern2> .. <filter patternN> )
//
// For example:
// "Images (*.gif *.png *.jpg)"
//
// Extract the contents of the <filter pattern>(s) inside the parenthesis and split them based on a whitespace or comma
const QRegularExpression filterContent(".*\\((?<filters>[^\\)]+)\\)");
QRegularExpressionMatch filterContentMatch = filterContent.match(selectedFilter);
if (!filterContentMatch.hasMatch())
{
return false;
}
QString filterExtensionsString = filterContentMatch.captured("filters");
QStringList filterExtensionsFull = filterExtensionsString.split(" ", Qt::SkipEmptyParts);
if (filterExtensionsFull.length() <= 0)
{
return false;
}
// If there are multiple suffixes in the selected filter, then default to the first one if a suffix needs to be appended
QString defaultSuffix = filterExtensionsFull[0].mid(1);
// Iterate through the filter patterns to see if the current filename matches
QFileInfo fileInfo(filePath);
bool extensionNeeded = true;
for (const QString& filterExtensionFull : filterExtensionsFull)
{
QString wildcardExpression = QRegularExpression::wildcardToRegularExpression(filterExtensionFull);
QRegularExpression filterPattern(wildcardExpression, AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY);
QRegularExpressionMatch filterPatternMatch = filterPattern.match(fileInfo.fileName());
if (filterPatternMatch.hasMatch())
{
// The filename matches one of the filter patterns already, the extension does not need to be added to the filename
extensionNeeded = false;
}
}
if (extensionNeeded)
{
// If the current (if any) suffix does not match, automatically add the default suffix for the selected filter
filePath.append(defaultSuffix);
}
return extensionNeeded;
}
} // namespace AzQtComponents

@ -24,6 +24,12 @@ namespace AzQtComponents
static QString GetSaveFileName(QWidget* parent = nullptr, const QString& caption = QString(),
const QString& dir = QString(), const QString& filter = QString(),
QString* selectedFilter = nullptr, QFileDialog::Options options = QFileDialog::Options());
//! Helper method that parses a selected filter from Qt's QFileDialog::getSaveFileName and applies the
//! selected filter's extension to the filePath if it doesnt already have the extension. This is needed
//! on platforms that do not have a default file dialog (These platforms uses Qt's custom file dialog which will
//! not apply the filter's extension automatically on user entered filenames)
static bool ApplyMissingExtension(const QString& selectedFilter, QString& filePath);
};
} // namespace AzQtComponents

@ -12,4 +12,6 @@ set(FILES
../../Utilities/QtWindowUtilities_linux.cpp
../../Utilities/ScreenGrabber_linux.cpp
../../../Platform/Linux/AzQtComponents/Components/StyledDockWidget_Linux.cpp
../../../Platform/Linux/AzQtComponents/AzQtComponents_Traits_Linux.h
../../../Platform/Linux/AzQtComponents/AzQtComponents_Traits_Platform.h
)

@ -12,4 +12,6 @@ set(FILES
../../Utilities/QtWindowUtilities_mac.mm
../../Utilities/ScreenGrabber_mac.mm
../../../Platform/Mac/AzQtComponents/Components/StyledDockWidget_Mac.cpp
../../../Platform/Mac/AzQtComponents/AzQtComponents_Traits_Mac.h
../../../Platform/Mac/AzQtComponents/AzQtComponents_Traits_Platform.h
)

@ -17,4 +17,6 @@ set(FILES
../../Components/TitleBarOverdrawScreenHandler_win.h
../../Components/TitleBarOverdrawScreenHandler_win.cpp
../../../Platform/Windows/AzQtComponents/Components/StyledDockWidget_Windows.cpp
../../../Platform/Windows/AzQtComponents/AzQtComponents_Traits_Windows.h
../../../Platform/Windows/AzQtComponents/AzQtComponents_Traits_Platform.h
)

@ -0,0 +1,65 @@
/*
* 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 <AzTest/AzTest.h>
#include <AzQtComponents/Components/Widgets/FileDialog.h>
#include <QString>
TEST(AzQtComponents, ApplyMissingExtension_UpdateMissingExtension_Success)
{
const QString textFiler{"Text Files (*.txt)"};
QString testPath{"testFile"};
bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath);
EXPECT_TRUE(result);
EXPECT_STRCASEEQ("testFile.txt", testPath.toUtf8().constData());
}
TEST(AzQtComponents, ApplyMissingExtension_NoUpdateExistingExtension_Success)
{
const QString textFiler{"Text Files (*.txt)"};
QString testPath{"testFile.txt"};
bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath);
EXPECT_FALSE(result);
EXPECT_STRCASEEQ("testFile.txt", testPath.toUtf8().constData());
}
TEST(AzQtComponents, ApplyMissingExtension_UpdateMissingExtensionMultipleExtensionFilter_Success)
{
const QString textFiler{"Image Files (*.jpg *.bmp *.png)"};
QString testPath{"testFile"};
bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath);
EXPECT_TRUE(result);
EXPECT_STRCASEEQ("testFile.jpg", testPath.toUtf8().constData());
}
TEST(AzQtComponents, ApplyMissingExtension_NoUpdateMissingExtensionMultipleExtensionFilter_Success)
{
const QString textFiler{"Image Files (*.jpg *.bmp *.png)"};
QString testPath{"testFile.png"};
bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath);
EXPECT_FALSE(result);
EXPECT_STRCASEEQ("testFile.png", testPath.toUtf8().constData());
}
TEST(AzQtComponents, ApplyMissingExtension_NoUpdateMissingExtensionEmptyFilter_Success)
{
const QString textFiler{""};
QString testPath{"testFile"};
bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath);
EXPECT_FALSE(result);
EXPECT_STRCASEEQ("testFile", testPath.toUtf8().constData());
}
TEST(AzQtComponents, ApplyMissingExtension_NoUpdateMissingExtensionInvalidFilter_Success)
{
const QString textFiler{"Bad Filter!!"};
QString testPath{"testFile"};
bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath);
EXPECT_FALSE(result);
EXPECT_STRCASEEQ("testFile", testPath.toUtf8().constData());
}

@ -9,6 +9,7 @@
set(FILES
Tests/AzQtComponentTests.cpp
Tests/ColorControllerTests.cpp
Tests/FileDialogTests.cpp
Tests/FloatToStringConversionTests.cpp
Tests/HexParsingTests.cpp
Tests/StyleSheetCacheTests.cpp

@ -10,6 +10,8 @@ if(NOT PAL_TRAIT_BUILD_HOST_TOOLS)
return()
endif()
ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME})
ly_add_target(
NAME AzQtComponents SHARED
NAMESPACE AZ
@ -26,6 +28,7 @@ ly_add_target(
AzQtComponents
PUBLIC
.
${pal_dir}
COMPILE_DEFINITIONS
PRIVATE
AZ_QT_COMPONENTS_EXPORT_SYMBOLS
@ -53,6 +56,7 @@ ly_add_target(
.
AzQtComponents
AzQtComponents/Gallery
${pal_dir}
BUILD_DEPENDENCIES
PRIVATE
3rdParty::Qt::Svg
@ -86,6 +90,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
PRIVATE
Tests
AzQtComponents
${pal_dir}
BUILD_DEPENDENCIES
PRIVATE
AZ::AzQtComponents

@ -0,0 +1,11 @@
/*
* 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
#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION 1
#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY QRegularExpression::NoPatternOption

@ -0,0 +1,10 @@
/*
* 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 <AzQtComponents/AzQtComponents_Traits_Linux.h>

@ -0,0 +1,11 @@
/*
* 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
#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION 0
#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY QRegularExpression::NoPatternOption

@ -0,0 +1,10 @@
/*
* 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 <AzQtComponents/AzQtComponents_Traits_Mac.h>

@ -0,0 +1,10 @@
/*
* 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 <AzQtComponents/AzQtComponents_Traits_Windows.h>

@ -0,0 +1,11 @@
/*
* 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
#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION 0
#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY QRegularExpression::CaseInsensitiveOption

@ -69,6 +69,7 @@
#include <AzToolsFramework/UI/EditorEntityUi/EditorEntityUiSystemComponent.h>
#include <AzToolsFramework/Undo/UndoCacheInterface.h>
#include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
#include <Entity/EntityUtilityComponent.h>
#include <QtWidgets/QMessageBox>
AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") // 4251: 'QFileInfo::d_ptr': class 'QSharedDataPointer<QFileInfoPrivate>' needs to have dll-interface to be used by clients of class 'QFileInfo'
@ -271,7 +272,8 @@ namespace AzToolsFramework
azrtti_typeid<AzToolsFramework::EditorInteractionSystemComponent>(),
azrtti_typeid<Components::EditorEntitySearchComponent>(),
azrtti_typeid<Components::EditorIntersectorComponent>(),
azrtti_typeid<AzToolsFramework::SliceRequestComponent>()
azrtti_typeid<AzToolsFramework::SliceRequestComponent>(),
azrtti_typeid<AzToolsFramework::EntityUtilityComponent>()
});
return components;

@ -410,7 +410,7 @@ namespace AzToolsFramework
filter.append(ext);
if (i < n - 1)
{
filter.append(", ");
filter.append(" ");
}
}
filter.append(")");

@ -53,6 +53,7 @@
#include <AzToolsFramework/Thumbnails/ThumbnailerNullComponent.h>
#include <AzToolsFramework/AssetBrowser/AssetBrowserComponent.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemComponent.h>
#include <AzToolsFramework/Entity/EntityUtilityComponent.h>
AZ_DEFINE_BUDGET(AzToolsFramework);
@ -71,6 +72,7 @@ namespace AzToolsFramework
Components::EditorSelectionAccentSystemComponent::CreateDescriptor(),
EditorEntityContextComponent::CreateDescriptor(),
EditorEntityFixupComponent::CreateDescriptor(),
EntityUtilityComponent::CreateDescriptor(),
ContainerEntitySystemComponent::CreateDescriptor(),
FocusModeSystemComponent::CreateDescriptor(),
SliceMetadataEntityContextComponent::CreateDescriptor(),

@ -0,0 +1,351 @@
/*
* 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 <sstream>
#include <AzCore/JSON/rapidjson.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/JsonSerializationSettings.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzFramework/Entity/EntityContext.h>
#include <AzFramework/FileFunc/FileFunc.h>
#include <AzToolsFramework/Entity/EntityUtilityComponent.h>
#include <Entity/EditorEntityContextBus.h>
#include <rapidjson/document.h>
namespace AzToolsFramework
{
void ComponentDetails::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<ComponentDetails>()
->Field("TypeInfo", &ComponentDetails::m_typeInfo)
->Field("BaseClasses", &ComponentDetails::m_baseClasses);
serializeContext->RegisterGenericType<AZStd::vector<ComponentDetails>>();
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<ComponentDetails>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "entity")
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All)
->Property("TypeInfo", BehaviorValueProperty(&ComponentDetails::m_typeInfo))
->Property("BaseClasses", BehaviorValueProperty(&ComponentDetails::m_baseClasses))
->Method("__repr__", [](const ComponentDetails& obj)
{
std::ostringstream result;
bool first = true;
for (const auto& baseClass : obj.m_baseClasses)
{
if (!first)
{
result << ", ";
}
first = false;
result << baseClass.c_str();
}
return AZStd::string::format("%s, Base Classes: <%s>", obj.m_typeInfo.c_str(), result.str().c_str());
})
->Attribute(AZ::Script::Attributes::Operator, AZ::Script::Attributes::OperatorType::ToString);
}
}
AZ::EntityId EntityUtilityComponent::CreateEditorReadyEntity(const AZStd::string& entityName)
{
auto* newEntity = m_entityContext->CreateEntity(entityName.c_str());
if (!newEntity)
{
AZ_Error("EditorEntityUtility", false, "Failed to create new entity %s", entityName.c_str());
return AZ::EntityId();
}
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
&AzToolsFramework::EditorEntityContextRequestBus::Events::AddRequiredComponents, *newEntity);
newEntity->Init();
auto newEntityId = newEntity->GetId();
m_createdEntities.emplace_back(newEntityId);
return newEntityId;
}
AZ::TypeId GetComponentTypeIdFromName(const AZStd::string& typeName)
{
// Try to create a TypeId first. We won't show any warnings if this fails as the input might be a class name instead
AZ::TypeId typeId = AZ::TypeId::CreateStringPermissive(typeName.data());
// If the typeId is null, try a lookup by class name
if (typeId.IsNull())
{
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
auto typeNameCrc = AZ::Crc32(typeName.data());
auto typeUuidList = serializeContext->FindClassId(typeNameCrc);
// TypeId is invalid or class name is invalid
if (typeUuidList.empty())
{
AZ_Error("EntityUtilityComponent", false, "Provided type %s is either an invalid TypeId or does not match any class names", typeName.c_str());
return AZ::TypeId::CreateNull();
}
typeId = typeUuidList[0];
}
return typeId;
}
AZ::Component* FindComponentHelper(AZ::EntityId entityId, const AZ::TypeId& typeId, AZ::ComponentId componentId, bool createComponent = false)
{
AZ::Entity* entity = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, entityId);
if (!entity)
{
AZ_Error("EntityUtilityComponent", false, "Invalid entityId %s", entityId.ToString().c_str());
return nullptr;
}
AZ::Component* component = nullptr;
if (componentId != AZ::InvalidComponentId)
{
component = entity->FindComponent(componentId);
}
else
{
component = entity->FindComponent(typeId);
}
if (!component && createComponent)
{
component = entity->CreateComponent(typeId);
}
if (!component)
{
AZ_Error(
"EntityUtilityComponent", false, "Failed to find component (%s) on entity %s (%s)",
componentId != AZ::InvalidComponentId ? AZStd::to_string(componentId).c_str()
: typeId.ToString<AZStd::string>().c_str(),
entityId.ToString().c_str(),
entity->GetName().c_str());
return nullptr;
}
return component;
}
AzFramework::BehaviorComponentId EntityUtilityComponent::GetOrAddComponentByTypeName(AZ::EntityId entityId, const AZStd::string& typeName)
{
AZ::TypeId typeId = GetComponentTypeIdFromName(typeName);
if (typeId.IsNull())
{
return AzFramework::BehaviorComponentId(AZ::InvalidComponentId);
}
AZ::Component* component = FindComponentHelper(entityId, typeId, AZ::InvalidComponentId, true);
return component ? AzFramework::BehaviorComponentId(component->GetId()) :
AzFramework::BehaviorComponentId(AZ::InvalidComponentId);
}
bool EntityUtilityComponent::UpdateComponentForEntity(AZ::EntityId entityId, AzFramework::BehaviorComponentId componentId, const AZStd::string& json)
{
if (!componentId.IsValid())
{
AZ_Error("EntityUtilityComponent", false, "Invalid componentId passed to UpdateComponentForEntity");
return false;
}
AZ::Component* component = FindComponentHelper(entityId, AZ::TypeId::CreateNull(), componentId);
if (!component)
{
return false;
}
using namespace AZ::JsonSerializationResult;
AZ::JsonDeserializerSettings settings = AZ::JsonDeserializerSettings{};
settings.m_reporting = []([[maybe_unused]] AZStd::string_view message, ResultCode result, AZStd::string_view) -> auto
{
if (result.GetProcessing() == Processing::Halted)
{
AZ_Error("EntityUtilityComponent", false, "JSON %s\n", message.data());
}
else if (result.GetOutcome() > Outcomes::PartialDefaults)
{
AZ_Warning("EntityUtilityComponent", false, "JSON %s\n", message.data());
}
return result;
};
rapidjson::Document doc;
doc.Parse<rapidjson::kParseCommentsFlag>(json.data(), json.size());
ResultCode resultCode = AZ::JsonSerialization::Load(*component, doc, settings);
return resultCode.GetProcessing() != Processing::Halted;
}
AZStd::string EntityUtilityComponent::GetComponentDefaultJson(const AZStd::string& typeName)
{
AZ::TypeId typeId = GetComponentTypeIdFromName(typeName);
if (typeId.IsNull())
{
// GetComponentTypeIdFromName already does error handling
return "";
}
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
const AZ::SerializeContext::ClassData* classData = serializeContext->FindClassData(typeId);
if (!classData)
{
AZ_Error("EntityUtilityComponent", false, "Failed to find ClassData for typeId %s (%s)", typeId.ToString<AZStd::string>().c_str(), typeName.c_str());
return "";
}
void* component = classData->m_factory->Create("Component");
rapidjson::Document document;
AZ::JsonSerializerSettings settings;
settings.m_keepDefaults = true;
auto resultCode = AZ::JsonSerialization::Store(document, document.GetAllocator(), component, nullptr, typeId, settings);
// Clean up the allocated component ASAP, we don't need it anymore
classData->m_factory->Destroy(component);
if (resultCode.GetProcessing() == AZ::JsonSerializationResult::Processing::Halted)
{
AZ_Error("EntityUtilityComponent", false, "Failed to serialize component to json (%s): %s",
typeName.c_str(), resultCode.ToString(typeName).c_str())
return "";
}
AZStd::string jsonString;
AZ::Outcome<void, AZStd::string> outcome = AZ::JsonSerializationUtils::WriteJsonString(document, jsonString);
if (!outcome.IsSuccess())
{
AZ_Error("EntityUtilityComponent", false, "Failed to write component json to string: %s", outcome.GetError().c_str());
return "";
}
return jsonString;
}
AZStd::vector<ComponentDetails> EntityUtilityComponent::FindMatchingComponents(const AZStd::string& searchTerm)
{
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
if (m_typeInfo.empty())
{
serializeContext->EnumerateDerived<AZ::Component>(
[this, serializeContext](const AZ::SerializeContext::ClassData* classData, const AZ::Uuid& /*typeId*/)
{
auto& typeInfo = m_typeInfo.emplace_back(classData->m_typeId, classData->m_name, AZStd::vector<AZStd::string>{});
serializeContext->EnumerateBase(
[&typeInfo](const AZ::SerializeContext::ClassData* classData, const AZ::Uuid&)
{
if (classData)
{
AZStd::get<2>(typeInfo).emplace_back(classData->m_name);
}
return true;
},
classData->m_typeId);
return true;
});
}
AZStd::vector<ComponentDetails> matches;
for (const auto& [typeId, typeName, baseClasses] : m_typeInfo)
{
if (AZStd::wildcard_match(searchTerm, typeName))
{
ComponentDetails details;
details.m_typeInfo = AZStd::string::format("%s %s", typeId.ToString<AZStd::string>().c_str(), typeName.c_str());
details.m_baseClasses = baseClasses;
matches.emplace_back(AZStd::move(details));
}
}
return matches;
}
void EntityUtilityComponent::ResetEntityContext()
{
for (AZ::EntityId entityId : m_createdEntities)
{
m_entityContext->DestroyEntityById(entityId);
}
m_createdEntities.clear();
m_entityContext->ResetContext();
}
void EntityUtilityComponent::Reflect(AZ::ReflectContext* context)
{
ComponentDetails::Reflect(context);
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<EntityUtilityComponent, AZ::Component>();
}
if (auto* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty("InvalidComponentId", BehaviorConstant(AZ::InvalidComponentId))
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Category, "Entity")
->Attribute(AZ::Script::Attributes::Module, "entity");
behaviorContext->EBus<EntityUtilityBus>("EntityUtilityBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All)
->Attribute(AZ::Script::Attributes::Category, "Entity")
->Attribute(AZ::Script::Attributes::Module, "entity")
->Event("CreateEditorReadyEntity", &EntityUtilityBus::Events::CreateEditorReadyEntity)
->Event("GetOrAddComponentByTypeName", &EntityUtilityBus::Events::GetOrAddComponentByTypeName)
->Event("UpdateComponentForEntity", &EntityUtilityBus::Events::UpdateComponentForEntity)
->Event("FindMatchingComponents", &EntityUtilityBus::Events::FindMatchingComponents)
->Event("GetComponentDefaultJson", &EntityUtilityBus::Events::GetComponentDefaultJson)
;
}
}
void EntityUtilityComponent::Activate()
{
m_entityContext = AZStd::make_unique<AzFramework::EntityContext>(UtilityEntityContextId);
m_entityContext->InitContext();
EntityUtilityBus::Handler::BusConnect();
}
void EntityUtilityComponent::Deactivate()
{
EntityUtilityBus::Handler::BusDisconnect();
m_entityContext = nullptr;
}
}

@ -0,0 +1,90 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Component/Component.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
#include <AzToolsFramework/ToolsComponents/EditorDisabledCompositionBus.h>
#include <AzToolsFramework/ToolsComponents/EditorPendingCompositionBus.h>
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
#include <AzCore/Component/ComponentApplication.h>
#include <AzFramework/Entity/BehaviorEntity.h>
namespace AzToolsFramework
{
struct ComponentDetails
{
AZ_TYPE_INFO(AzToolsFramework::ComponentDetails, "{107D8379-4AD4-4547-BEE1-184B120F23E9}");
static void Reflect(AZ::ReflectContext* context);
AZStd::string m_typeInfo;
AZStd::vector<AZStd::string> m_baseClasses;
};
// This ebus is intended to provide behavior-context friendly APIs to create and manage entities
struct EntityUtilityTraits : AZ::EBusTraits
{
AZ_RTTI(AzToolsFramework::EntityUtilityTraits, "{A6305CAE-C825-43F9-A44D-E503910912AF}");
virtual ~EntityUtilityTraits() = default;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
// Creates an entity with the default editor components attached and initializes the entity
virtual AZ::EntityId CreateEditorReadyEntity(const AZStd::string& entityName) = 0;
virtual AzFramework::BehaviorComponentId GetOrAddComponentByTypeName(AZ::EntityId entity, const AZStd::string& typeName) = 0;
virtual bool UpdateComponentForEntity(AZ::EntityId entity, AzFramework::BehaviorComponentId component, const AZStd::string& json) = 0;
// Gets a JSON string containing describing the default serialization state of the specified component
virtual AZStd::string GetComponentDefaultJson(const AZStd::string& typeName) = 0;
// Returns a list of matching component type names. Supports wildcard search terms
virtual AZStd::vector<ComponentDetails> FindMatchingComponents(const AZStd::string& searchTerm) = 0;
virtual void ResetEntityContext() = 0;
};
using EntityUtilityBus = AZ::EBus<EntityUtilityTraits>;
struct EntityUtilityComponent : AZ::Component
, EntityUtilityBus::Handler
{
inline const static AZ::Uuid UtilityEntityContextId = AZ::Uuid("{9C277B88-E79E-4F8A-BAFF-A4C175BD565F}");
AZ_COMPONENT(EntityUtilityComponent, "{47205907-A0EA-4FFF-A620-04D20C04A379}");
AZ::EntityId CreateEditorReadyEntity(const AZStd::string& entityName) override;
AzFramework::BehaviorComponentId GetOrAddComponentByTypeName(AZ::EntityId entity, const AZStd::string& typeName) override;
bool UpdateComponentForEntity(AZ::EntityId entity, AzFramework::BehaviorComponentId component, const AZStd::string& json) override;
AZStd::string GetComponentDefaultJson(const AZStd::string& typeName) override;
AZStd::vector<ComponentDetails> FindMatchingComponents(const AZStd::string& searchTerm) override;
void ResetEntityContext() override;
static void Reflect(AZ::ReflectContext* context);
protected:
void Activate() override;
void Deactivate() override;
// Our own entity context. This API is intended mostly for use in Asset Builders where there is no editor context
// Additionally, an entity context is needed when using the Behavior Entity class
AZStd::unique_ptr<AzFramework::EntityContext> m_entityContext;
// TypeId, TypeName, Vector<BaseClassName>
AZStd::vector<AZStd::tuple<AZ::TypeId, AZStd::string, AZStd::vector<AZStd::string>>> m_typeInfo;
// Keep track of the entities we create so they can be reset
AZStd::vector<AZ::EntityId> m_createdEntities;
};
}; // namespace AzToolsFramework

@ -10,6 +10,7 @@
#include <AzCore/Component/EntityId.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Entity/EntityContextBus.h>

@ -224,6 +224,7 @@ namespace AzToolsFramework
return false;
}
AZ::Data::SerializedAssetTracker* assetTracker = settings.m_metadata.Find<AZ::Data::SerializedAssetTracker>();
referencedAssets = AZStd::move(assetTracker->GetTrackedAssets());
@ -245,6 +246,30 @@ namespace AzToolsFramework
entityIdMapper.SetEntityIdGenerationApproach(InstanceEntityIdMapper::EntityIdGenerationApproach::Random);
}
// some assets may come in from the JSON serialzier with no AssetID, but have an asset hint
// this attempts to fix up the assets using the assetHint field
auto fixUpInvalidAssets = [](AZ::Data::Asset<AZ::Data::AssetData>& asset)
{
if (!asset.GetId().IsValid() && !asset.GetHint().empty())
{
AZ::Data::AssetId assetId;
AZ::Data::AssetCatalogRequestBus::BroadcastResult(
assetId,
&AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
asset.GetHint().c_str(),
AZ::Data::s_invalidAssetType,
false);
if (assetId.IsValid())
{
asset.Create(assetId, true);
}
}
};
auto tracker = AZ::Data::SerializedAssetTracker{};
tracker.SetAssetFixUp(fixUpInvalidAssets);
AZ::JsonDeserializerSettings settings;
// The InstanceEntityIdMapper is registered twice because it's used in several places during deserialization where one is
// specific for the InstanceEntityIdMapper and once for the generic JsonEntityIdMapper. Because the Json Serializer's meta
@ -252,16 +277,17 @@ namespace AzToolsFramework
settings.m_metadata.Add(static_cast<AZ::JsonEntityIdSerializer::JsonEntityIdMapper*>(&entityIdMapper));
settings.m_metadata.Add(&entityIdMapper);
settings.m_metadata.Create<InstanceEntityScrubber>(newlyAddedEntities);
settings.m_metadata.Add(tracker);
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](
AZStd::string_view message, AZ::JsonSerializationResult::ResultCode result,
AZStd::string_view path) -> AZ::JsonSerializationResult::ResultCode
AZStd::string_view message, AZ::JsonSerializationResult::ResultCode result,
AZStd::string_view path) -> AZ::JsonSerializationResult::ResultCode
{
return Internal::JsonIssueReporter(scratchBuffer, message, result, path);
};
settings.m_reporting = AZStd::move(issueReportingCallback);
AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::Load(instance, prefabDom, settings);
AZ::Data::AssetManager::Instance().ResumeAssetRelease();

@ -50,17 +50,19 @@ namespace AzToolsFramework
auto settingsRegistry = AZ::SettingsRegistry::Get();
AZ_Assert(settingsRegistry, "Settings registry is not set");
[[maybe_unused]] bool result =
settingsRegistry->Get(m_projectPathWithOsSeparator.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath);
AZ_Warning("Prefab", result, "Couldn't retrieve project root path");
m_projectPathWithSlashSeparator = AZ::IO::Path(m_projectPathWithOsSeparator.Native(), '/').MakePreferred();
AZ::Interface<PrefabLoaderInterface>::Register(this);
m_scriptingPrefabLoader.Connect(this);
}
void PrefabLoader::UnregisterPrefabLoaderInterface()
{
m_scriptingPrefabLoader.Disconnect();
AZ::Interface<PrefabLoaderInterface>::Unregister(this);
}
@ -568,7 +570,7 @@ namespace AzToolsFramework
(pathStr.find_first_of(AZ_FILESYSTEM_INVALID_CHARACTERS) == AZStd::string::npos) &&
(pathStr.back() != '\\' && pathStr.back() != '/');
}
AZ::IO::Path PrefabLoader::GetFullPath(AZ::IO::PathView path)
{
AZ::IO::Path pathWithOSSeparator = AZ::IO::Path(path).MakePreferred();
@ -596,26 +598,38 @@ namespace AzToolsFramework
{
// The asset system provided us with a valid root folder and relative path, so return it.
fullPath = AZ::IO::Path(rootFolder) / assetInfo.m_relativePath;
return fullPath;
}
else
{
// If for some reason the Asset system couldn't provide a relative path, provide some fallback logic.
// attempt to find the absolute from the Cache folder
AZStd::string assetRootFolder;
if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
{
settingsRegistry->Get(assetRootFolder, AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder);
}
fullPath = AZ::IO::Path(assetRootFolder) / path;
if (fullPath.IsAbsolute() && AZ::IO::SystemFile::Exists(fullPath.c_str()))
{
return fullPath;
}
}
// Check to see if the AssetProcessor is ready. If it *is* and we didn't get a path, print an error then follow
// the fallback logic. If it's *not* ready, we're probably either extremely early in a tool startup flow or inside
// a unit test, so just execute the fallback logic without an error.
[[maybe_unused]] bool assetProcessorReady = false;
AzFramework::AssetSystemRequestBus::BroadcastResult(
assetProcessorReady, &AzFramework::AssetSystemRequestBus::Events::AssetProcessorIsReady);
// If for some reason the Asset system couldn't provide a relative path, provide some fallback logic.
AZ_Error(
"Prefab", !assetProcessorReady, "Full source path for '%.*s' could not be determined. Using fallback logic.",
AZ_STRING_ARG(path.Native()));
// Check to see if the AssetProcessor is ready. If it *is* and we didn't get a path, print an error then follow
// the fallback logic. If it's *not* ready, we're probably either extremely early in a tool startup flow or inside
// a unit test, so just execute the fallback logic without an error.
[[maybe_unused]] bool assetProcessorReady = false;
AzFramework::AssetSystemRequestBus::BroadcastResult(
assetProcessorReady, &AzFramework::AssetSystemRequestBus::Events::AssetProcessorIsReady);
// If a relative path was passed in, make it relative to the project root.
fullPath = AZ::IO::Path(m_projectPathWithOsSeparator).Append(pathWithOSSeparator);
}
AZ_Error(
"Prefab", !assetProcessorReady, "Full source path for '%.*s' could not be determined. Using fallback logic.",
AZ_STRING_ARG(path.Native()));
// If a relative path was passed in, make it relative to the project root.
fullPath = AZ::IO::Path(m_projectPathWithOsSeparator).Append(pathWithOSSeparator);
return fullPath;
}

@ -15,6 +15,7 @@
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/string/string.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
#include <Prefab/ScriptingPrefabLoader.h>
namespace AZ
{
@ -114,6 +115,7 @@ namespace AzToolsFramework
void SetSaveAllPrefabsPreference(SaveAllPrefabsPreference saveAllPrefabsPreference) override;
private:
/**
* Copies the template dom provided and manipulates it into the proper format to be saved to disk.
* @param templateRef The template whose dom we want to transform into the proper format to be saved to disk.
@ -177,6 +179,7 @@ namespace AzToolsFramework
AZStd::optional<AZStd::pair<PrefabDom, AZ::IO::Path>> StoreTemplateIntoFileFormat(TemplateId templateId);
PrefabSystemComponentInterface* m_prefabSystemComponentInterface = nullptr;
ScriptingPrefabLoader m_scriptingPrefabLoader;
AZ::IO::Path m_projectPathWithOsSeparator;
AZ::IO::Path m_projectPathWithSlashSeparator;
};

@ -99,7 +99,6 @@ namespace AzToolsFramework
// Generates a new path
static AZ::IO::Path GeneratePath();
};
} // namespace Prefab
} // namespace AzToolsFramework

@ -0,0 +1,45 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Interface/Interface.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/Prefab/PrefabIdTypes.h>
#include <AzCore/EBus/EBus.h>
namespace AzToolsFramework
{
namespace Prefab
{
// Ebus for script-friendly APIs for the prefab loader
struct PrefabLoaderScriptingTraits : AZ::EBusTraits
{
AZ_TYPE_INFO(PrefabLoaderScriptingTraits, "{C344B7D8-8299-48C9-8450-26E1332EA011}");
virtual ~PrefabLoaderScriptingTraits() = default;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
/**
* Saves a Prefab Template into the provided output string.
* Converts Prefab Template form into .prefab form by collapsing nested Template info
* into a source path and patches.
* @param templateId Id of the template to be saved
* @return Will contain the serialized template json on success
*/
virtual AZ::Outcome<AZStd::string, void> SaveTemplateToString(TemplateId templateId) = 0;
};
using PrefabLoaderScriptingBus = AZ::EBus<PrefabLoaderScriptingTraits>;
} // namespace Prefab
} // namespace AzToolsFramework

@ -10,6 +10,7 @@
#include <AzCore/Component/Entity.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityIdMapper.h>
@ -36,12 +37,14 @@ namespace AzToolsFramework
m_instanceToTemplatePropagator.RegisterInstanceToTemplateInterface();
m_prefabPublicHandler.RegisterPrefabPublicHandlerInterface();
m_prefabPublicRequestHandler.Connect();
m_prefabSystemScriptingHandler.Connect(this);
AZ::SystemTickBus::Handler::BusConnect();
}
void PrefabSystemComponent::Deactivate()
{
AZ::SystemTickBus::Handler::BusDisconnect();
m_prefabSystemScriptingHandler.Disconnect();
m_prefabPublicRequestHandler.Disconnect();
m_prefabPublicHandler.UnregisterPrefabPublicHandlerInterface();
m_instanceToTemplatePropagator.UnregisterInstanceToTemplateInterface();
@ -58,13 +61,24 @@ namespace AzToolsFramework
AzToolsFramework::Prefab::PrefabConversionUtils::EditorInfoRemover::Reflect(context);
PrefabPublicRequestHandler::Reflect(context);
PrefabLoader::Reflect(context);
PrefabSystemScriptingHandler::Reflect(context);
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
if (serialize)
if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
{
serialize->Class<PrefabSystemComponent, AZ::Component>()->Version(1);
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<PrefabLoaderScriptingBus>("PrefabLoaderScriptingBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "prefab")
->Attribute(AZ::Script::Attributes::Category, "Prefab")
->Event("SaveTemplateToString", &PrefabLoaderScriptingBus::Events::SaveTemplateToString);
;
}
AZ::JsonRegistrationContext* jsonRegistration = azrtti_cast<AZ::JsonRegistrationContext*>(context);
if (jsonRegistration)
{
@ -145,7 +159,7 @@ namespace AzToolsFramework
newInstance->SetTemplateId(newTemplateId);
}
}
void PrefabSystemComponent::PropagateTemplateChanges(TemplateId templateId, bool immediate, InstanceOptionalReference instanceToExclude)
{
UpdatePrefabInstances(templateId, immediate, instanceToExclude);

@ -27,6 +27,7 @@
#include <AzToolsFramework/Prefab/PrefabPublicRequestHandler.h>
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
#include <Prefab/PrefabSystemScriptingHandler.h>
namespace AZ
{
@ -219,7 +220,7 @@ namespace AzToolsFramework
const AZStd::vector<AZ::Entity*>& entities, AZStd::vector<AZStd::unique_ptr<Instance>>&& instancesToConsume,
AZ::IO::PathView filePath, AZStd::unique_ptr<AZ::Entity> containerEntity = nullptr,
InstanceOptionalReference parent = AZStd::nullopt, bool shouldCreateLinks = true) override;
PrefabDom& FindTemplateDom(TemplateId templateId) override;
/**
@ -244,7 +245,7 @@ namespace AzToolsFramework
private:
AZ_DISABLE_COPY_MOVE(PrefabSystemComponent);
/**
* Builds a new Prefab Template out of entities and instances and returns the first instance comprised of
* these entities and instances.
@ -412,6 +413,8 @@ namespace AzToolsFramework
// Handler of the public Prefab requests.
PrefabPublicRequestHandler m_prefabPublicRequestHandler;
PrefabSystemScriptingHandler m_prefabSystemScriptingHandler;
};
} // namespace Prefab
} // namespace AzToolsFramework

@ -78,8 +78,7 @@ namespace AzToolsFramework
AZStd::unique_ptr<AZ::Entity> containerEntity = nullptr, InstanceOptionalReference parent = AZStd::nullopt,
bool shouldCreateLinks = true) = 0;
};
} // namespace Prefab
} // namespace AzToolsFramework

@ -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/Interface/Interface.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/Link/Link.h>
#include <AzToolsFramework/Prefab/PrefabIdTypes.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/EBus/EBus.h>
namespace AzToolsFramework
{
namespace Prefab
{
// Bus that exposes a script-friendly interface to the PrefabSystemComponent
struct PrefabSystemScriptingEbusTraits : AZ::EBusTraits
{
using MutexType = AZ::NullMutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
virtual TemplateId CreatePrefabTemplate(
const AZStd::vector<AZ::EntityId>& entityIds, const AZStd::string& filePath) = 0;
};
using PrefabSystemScriptingBus = AZ::EBus<PrefabSystemScriptingEbusTraits>;
} // namespace Prefab
} // namespace AzToolsFramework

@ -0,0 +1,75 @@
/*
* 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/Component/ComponentApplicationBus.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <Prefab/PrefabSystemComponentInterface.h>
#include <Prefab/PrefabSystemScriptingHandler.h>
#include <AzCore/Component/Entity.h>
namespace AzToolsFramework::Prefab
{
void PrefabSystemScriptingHandler::Reflect(AZ::ReflectContext* context)
{
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty("InvalidTemplateId", BehaviorConstant(InvalidTemplateId))
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "prefab")
->Attribute(AZ::Script::Attributes::Category, "Prefab");
behaviorContext->EBus<PrefabSystemScriptingBus>("PrefabSystemScriptingBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "prefab")
->Attribute(AZ::Script::Attributes::Category, "Prefab")
->Event("CreatePrefab", &PrefabSystemScriptingBus::Events::CreatePrefabTemplate);
}
}
void PrefabSystemScriptingHandler::Connect(PrefabSystemComponentInterface* prefabSystemComponentInterface)
{
AZ_Assert(prefabSystemComponentInterface != nullptr, "prefabSystemComponentInterface must not be null");
m_prefabSystemComponentInterface = prefabSystemComponentInterface;
PrefabSystemScriptingBus::Handler::BusConnect();
}
void PrefabSystemScriptingHandler::Disconnect()
{
PrefabSystemScriptingBus::Handler::BusDisconnect();
}
TemplateId PrefabSystemScriptingHandler::CreatePrefabTemplate(const AZStd::vector<AZ::EntityId>& entityIds, const AZStd::string& filePath)
{
AZStd::vector<AZ::Entity*> entities;
for (const auto& entityId : entityIds)
{
AZ::Entity* entity = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, entityId);
AZ_Warning(
"PrefabSystemComponent", entity, "EntityId %s was not found and will not be added to the prefab",
entityId.ToString().c_str());
if (entity)
{
entities.push_back(entity);
}
}
auto prefab = m_prefabSystemComponentInterface->CreatePrefab(entities, {}, AZ::IO::PathView(AZStd::string_view(filePath)));
if (!prefab)
{
AZ_Error("PrefabSystemComponenent", false, "Failed to create prefab %s", filePath.c_str());
return InvalidTemplateId;
}
return prefab->GetTemplateId();
}
}

@ -0,0 +1,39 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <Prefab/PrefabSystemScriptingBus.h>
namespace AzToolsFramework
{
namespace Prefab
{
class PrefabSystemScriptingHandler
: PrefabSystemScriptingBus::Handler
{
public:
static void Reflect(AZ::ReflectContext* context);
PrefabSystemScriptingHandler() = default;
void Connect(PrefabSystemComponentInterface* prefabSystemComponentInterface);
void Disconnect();
private:
AZ_DISABLE_COPY(PrefabSystemScriptingHandler);
//////////////////////////////////////////////////////////////////////////
// PrefabSystemScriptingBus implementation
TemplateId CreatePrefabTemplate(const AZStd::vector<AZ::EntityId>& entityIds, const AZStd::string& filePath) override;
//////////////////////////////////////////////////////////////////////////
PrefabSystemComponentInterface* m_prefabSystemComponentInterface = nullptr;
};
} // namespace Prefab
} // namespace AzToolsFramework

@ -0,0 +1,144 @@
/*
* 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 <Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzFramework/FileFunc/FileFunc.h>
namespace AZ::Prefab
{
static constexpr const char s_useProceduralPrefabsKey[] = "/O3DE/Preferences/Prefabs/UseProceduralPrefabs";
// ProceduralPrefabAsset
ProceduralPrefabAsset::ProceduralPrefabAsset(const AZ::Data::AssetId& assetId)
: AZ::Data::AssetData(assetId)
, m_templateId(AzToolsFramework::Prefab::InvalidTemplateId)
{
}
void ProceduralPrefabAsset::Reflect(AZ::ReflectContext* context)
{
PrefabDomData::Reflect(context);
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<ProceduralPrefabAsset, AZ::Data::AssetData>()
->Version(1)
->Field("Template Name", &ProceduralPrefabAsset::m_templateName)
->Field("Template ID", &ProceduralPrefabAsset::m_templateId);
}
}
const AZStd::string& ProceduralPrefabAsset::GetTemplateName() const
{
return m_templateName;
}
void ProceduralPrefabAsset::SetTemplateName(AZStd::string templateName)
{
m_templateName = AZStd::move(templateName);
}
AzToolsFramework::Prefab::TemplateId ProceduralPrefabAsset::GetTemplateId() const
{
return m_templateId;
}
void ProceduralPrefabAsset::SetTemplateId(AzToolsFramework::Prefab::TemplateId templateId)
{
m_templateId = templateId;
}
bool ProceduralPrefabAsset::UseProceduralPrefabs()
{
bool useProceduralPrefabs = false;
bool result = AZ::SettingsRegistry::Get()->GetObject(useProceduralPrefabs, s_useProceduralPrefabsKey);
return result && useProceduralPrefabs;
}
// PrefabDomData
void PrefabDomData::Reflect(AZ::ReflectContext* context)
{
if (auto* jsonContext = azrtti_cast<AZ::JsonRegistrationContext*>(context))
{
jsonContext->Serializer<PrefabDomDataJsonSerializer>()->HandlesType<PrefabDomData>();
}
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<PrefabDomData>()
->Version(1);
}
}
void PrefabDomData::CopyValue(const rapidjson::Value& inputValue)
{
m_prefabDom.CopyFrom(inputValue, m_prefabDom.GetAllocator());
}
const AzToolsFramework::Prefab::PrefabDom& PrefabDomData::GetValue() const
{
return m_prefabDom;
}
// PrefabDomDataJsonSerializer
AZ::JsonSerializationResult::Result PrefabDomDataJsonSerializer::Load(
void* outputValue,
[[maybe_unused]] const AZ::Uuid& outputValueTypeId,
const rapidjson::Value& inputValue,
AZ::JsonDeserializerContext& context)
{
AZ_Assert(outputValueTypeId == azrtti_typeid<PrefabDomData>(),
"PrefabDomDataJsonSerializer Load against output typeID that was not PrefabDomData");
AZ_Assert(outputValue, "PrefabDomDataJsonSerializer Load against null output");
namespace JSR = AZ::JsonSerializationResult;
JSR::ResultCode result(JSR::Tasks::ReadField);
if (inputValue.IsObject() == false)
{
result.Combine(context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Missing, "Missing object"));
return context.Report(result, "Prefab should be an object.");
}
if (inputValue.MemberCount() < 1)
{
result.Combine(context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Missing, "Missing members"));
return context.Report(result, "Prefab should have multiple members.");
}
auto* outputVariable = reinterpret_cast<PrefabDomData*>(outputValue);
outputVariable->CopyValue(inputValue);
return context.Report(result, "Loaded procedural prefab");
}
AZ::JsonSerializationResult::Result PrefabDomDataJsonSerializer::Store(
rapidjson::Value& outputValue,
const void* inputValue,
[[maybe_unused]] const void* defaultValue,
[[maybe_unused]] const AZ::Uuid& valueTypeId,
AZ::JsonSerializerContext& context)
{
AZ_Assert(inputValue, "Input value for PrefabDomDataJsonSerializer can't be null.");
AZ_Assert(azrtti_typeid<PrefabDomData>() == valueTypeId,
"Unable to Serialize because the provided type is not PrefabGroup::PrefabDomData.");
const PrefabDomData* prefabDomData = reinterpret_cast<const PrefabDomData*>(inputValue);
namespace JSR = AZ::JsonSerializationResult;
JSR::ResultCode result(JSR::Tasks::WriteValue);
outputValue.SetObject();
outputValue.CopyFrom(prefabDomData->GetValue(), context.GetJsonAllocator());
return context.Report(result, "Stored procedural prefab");
}
}

@ -0,0 +1,90 @@
/*
* 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/Asset/AssetCommon.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
#include <AzToolsFramework/Prefab/PrefabIdTypes.h>
#include <AzCore/Serialization/Json/BaseJsonSerializer.h>
namespace AZ::Prefab
{
//! A wrapper around the JSON DOM type so that the assets can read in and write out
//! JSON directly since Prefabs are JSON serialized entity-component data
class PrefabDomData final
{
public:
AZ_RTTI(PrefabDomData, "{C73A3360-D772-4D41-9118-A039BF9340C1}");
AZ_CLASS_ALLOCATOR(PrefabDomData, AZ::SystemAllocator, 0);
PrefabDomData() = default;
~PrefabDomData() = default;
static void Reflect(AZ::ReflectContext* context);
void CopyValue(const rapidjson::Value& inputValue);
const AzToolsFramework::Prefab::PrefabDom& GetValue() const;
private:
AzToolsFramework::Prefab::PrefabDom m_prefabDom;
};
//! Registered to help read/write JSON for the PrefabDomData::m_prefabDom
class PrefabDomDataJsonSerializer final
: public AZ::BaseJsonSerializer
{
public:
AZ_RTTI(PrefabDomDataJsonSerializer, "{9FC48652-A00B-4EFA-8FD9-345A8E625439}", BaseJsonSerializer);
AZ_CLASS_ALLOCATOR(PrefabDomDataJsonSerializer, AZ::SystemAllocator, 0);
~PrefabDomDataJsonSerializer() override = default;
AZ::JsonSerializationResult::Result Load(
void* outputValue,
const AZ::Uuid& outputValueTypeId,
const rapidjson::Value& inputValue,
AZ::JsonDeserializerContext& context) override;
AZ::JsonSerializationResult::Result Store(
rapidjson::Value& outputValue,
const void* inputValue,
const void* defaultValue,
const AZ::Uuid& valueTypeId,
AZ::JsonSerializerContext& context) override;
};
//! An asset type to register templates into the Prefab system so that they
//! can instantiate like Authored Prefabs
class ProceduralPrefabAsset
: public AZ::Data::AssetData
{
public:
AZ_CLASS_ALLOCATOR(ProceduralPrefabAsset, AZ::SystemAllocator, 0);
AZ_RTTI(ProceduralPrefabAsset, "{9B7C8459-471E-4EAD-A363-7990CC4065A9}", AZ::Data::AssetData);
static bool UseProceduralPrefabs();
ProceduralPrefabAsset(const AZ::Data::AssetId& assetId = AZ::Data::AssetId());
~ProceduralPrefabAsset() override = default;
ProceduralPrefabAsset(const ProceduralPrefabAsset& rhs) = delete;
ProceduralPrefabAsset& operator=(const ProceduralPrefabAsset& rhs) = delete;
const AZStd::string& GetTemplateName() const;
void SetTemplateName(AZStd::string templateName);
AzToolsFramework::Prefab::TemplateId GetTemplateId() const;
void SetTemplateId(AzToolsFramework::Prefab::TemplateId templateId);
static void Reflect(AZ::ReflectContext* context);
private:
AZStd::string m_templateName;
AzToolsFramework::Prefab::TemplateId m_templateId;
};
}

@ -0,0 +1,37 @@
/*
* 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 <Prefab/ScriptingPrefabLoader.h>
namespace AzToolsFramework::Prefab
{
void ScriptingPrefabLoader::Connect(PrefabLoaderInterface* prefabLoaderInterface)
{
AZ_Assert(prefabLoaderInterface, "prefabLoaderInterface must not be null");
m_prefabLoaderInterface = prefabLoaderInterface;
PrefabLoaderScriptingBus::Handler::BusConnect();
}
void ScriptingPrefabLoader::Disconnect()
{
PrefabLoaderScriptingBus::Handler::BusDisconnect();
}
AZ::Outcome<AZStd::string, void> ScriptingPrefabLoader::SaveTemplateToString(TemplateId templateId)
{
AZStd::string json;
if (m_prefabLoaderInterface->SaveTemplateToString(templateId, json))
{
return AZ::Success(json);
}
return AZ::Failure();
}
} // namespace AzToolsFramework::Prefab

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

Loading…
Cancel
Save