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

Signed-off-by: chcurran <82187351+carlitosan@users.noreply.github.com>
This commit is contained in:
chcurran
2021-11-11 16:04:56 -08:00
1944 changed files with 220327 additions and 6146 deletions
+1
View File
@@ -12,6 +12,7 @@ Editor/EditorEventLog.xml
Editor/EditorLayout.xml
**/*egg-info/**
**/*egg-link
**/[Rr]estricted
UserSettings.xml
[Uu]ser/
FrameCapture/**
@@ -2,6 +2,7 @@
"gem_name": "PythonCoverage",
"display_name": "PythonCoverage",
"license": "Apache-2.0 Or MIT",
"license_url": "https://github.com/o3de/o3de/blob/development/LICENSE.txt",
"origin": "Open 3D Engine - o3de.org",
"type": "Tool",
"summary": "A tool for generating gem coverage for Python tests.",
@@ -21,10 +21,18 @@ class TestAutomation(EditorTestSuite):
class AtomEditorComponents_DecalAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DecalAdded as test_module
@pytest.mark.test_case_id("C36525658")
class AtomEditorComponents_DeferredFogAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DeferredFogAdded 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("C36525659")
class AtomEditorComponents_DiffuseProbeGridAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DiffuseProbeGridAdded as test_module
@pytest.mark.test_case_id("C32078120")
class AtomEditorComponents_DirectionalLightAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DirectionalLightAdded as test_module
@@ -33,6 +41,10 @@ class TestAutomation(EditorTestSuite):
class AtomEditorComponents_DisplayMapperAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DisplayMapperAdded as test_module
@pytest.mark.test_case_id("C36525661")
class AtomEditorComponents_EntityReferenceAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_EntityReferenceAdded as test_module
@pytest.mark.test_case_id("C32078121")
class AtomEditorComponents_ExposureControlAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_ExposureControlAdded as test_module
@@ -90,5 +102,9 @@ class TestAutomation(EditorTestSuite):
class AtomEditorComponents_ReflectionProbeAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_ReflectionProbeAdded as test_module
@pytest.mark.test_case_id("C36525666")
class AtomEditorComponents_SSAOAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_SSAOAdded as test_module
class ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(EditorSharedTest):
from Atom.tests import hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges as test_module
@@ -85,12 +85,14 @@ class AtomComponentProperties:
Deferred Fog component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
- 'Enable Deferred Fog' Toggle active state of the component True/False
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Deferred Fog',
'requires': [AtomComponentProperties.postfx_layer()],
'Enable Deferred Fog': 'Controller|Configuration|Enable Deferred Fog',
}
return properties[property]
@@ -113,7 +115,7 @@ class AtomComponentProperties:
return properties[property]
@staticmethod
def diffuse_probe(property: str = 'name') -> str:
def diffuse_probe_grid(property: str = 'name') -> str:
"""
Diffuse Probe Grid component properties. Requires one of 'shapes'.
- 'shapes' a list of supported shapes as component names.
@@ -0,0 +1,193 @@
"""
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")
deferred_fog_creation = (
"Deferred Fog Entity successfully created",
"Deferred Fog Entity failed to be created")
deferred_fog_component = (
"Entity has a Deferred Fog component",
"Entity failed to find Deferred Fog component")
deferred_fog_disabled = (
"Deferred Fog component disabled",
"Deferred Fog component was not disabled")
postfx_layer_component = (
"Entity has a PostFX Layer component",
"Entity did not have an PostFX Layer component")
deferred_fog_enabled = (
"Deferred Fog component enabled",
"Deferred Fog component was not enabled")
enable_deferred_fog_parameter_enabled = (
"Enable Deferred Fog parameter enabled",
"Enable Deferred Fog parameter 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_DeferredFog_AddedToEntity():
"""
Summary:
Tests the Deferred Fog 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 an Deferred Fog entity with no components.
2) Add Deferred Fog component to Deferred Fog entity.
3) UNDO the entity creation and component addition.
4) REDO the entity creation and component addition.
5) Verify Deferred Fog component not enabled.
6) Add PostFX Layer component since it is required by the Deferred Fog component.
7) Verify Deferred Fog component is enabled.
8) Enable the "Enable Deferred Fog" parameter.
9) Enter/Exit game mode.
10) Test IsHidden.
11) Test IsVisible.
12) Delete Deferred Fog entity.
13) UNDO deletion.
14) REDO deletion.
15) Look for errors.
:return: None
"""
import azlmbr.legacy.general as general
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Test steps begin.
# 1. Create an Deferred Fog entity with no components.
deferred_fog_entity = EditorEntity.create_editor_entity(AtomComponentProperties.deferred_fog())
Report.critical_result(Tests.deferred_fog_creation, deferred_fog_entity.exists())
# 2. Add Deferred Fog component to Deferred Fog entity.
deferred_fog_component = deferred_fog_entity.add_component(
AtomComponentProperties.deferred_fog())
Report.critical_result(
Tests.deferred_fog_component,
deferred_fog_entity.has_component(AtomComponentProperties.deferred_fog()))
# 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 deferred_fog_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, deferred_fog_entity.exists())
# 5. Verify Deferred Fog component not enabled.
Report.result(Tests.deferred_fog_disabled, not deferred_fog_component.is_enabled())
# 6. Add PostFX Layer component since it is required by the Deferred Fog component.
deferred_fog_entity.add_component(AtomComponentProperties.postfx_layer())
Report.result(
Tests.postfx_layer_component,
deferred_fog_entity.has_component(AtomComponentProperties.postfx_layer()))
# 7. Verify Deferred Fog component is enabled.
Report.result(Tests.deferred_fog_enabled, deferred_fog_component.is_enabled())
# 8. Enable the "Enable Deferred Fog" parameter.
deferred_fog_component.set_component_property_value(
AtomComponentProperties.deferred_fog('Enable Deferred Fog'), True)
Report.result(Tests.enable_deferred_fog_parameter_enabled,
deferred_fog_component.get_component_property_value(
AtomComponentProperties.deferred_fog('Enable Deferred Fog')) is True)
# 9. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 10. Test IsHidden.
deferred_fog_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, deferred_fog_entity.is_hidden() is True)
# 11. Test IsVisible.
deferred_fog_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, deferred_fog_entity.is_visible() is True)
# 12. Delete Deferred Fog entity.
deferred_fog_entity.delete()
Report.result(Tests.entity_deleted, not deferred_fog_entity.exists())
# 13. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, deferred_fog_entity.exists())
# 14. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not deferred_fog_entity.exists())
# 15. Look for errors and asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponents_DeferredFog_AddedToEntity)
@@ -0,0 +1,187 @@
"""
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")
diffuse_probe_grid_creation = (
"Diffuse Probe Grid Entity successfully created",
"Diffuse Probe Grid Entity failed to be created")
diffuse_probe_grid_component = (
"Entity has a Diffuse Probe Grid component",
"Entity failed to find Diffuse Probe Grid component")
diffuse_probe_grid_disabled = (
"Diffuse Probe Grid component disabled",
"Diffuse Probe Grid component was not disabled")
diffuse_probe_grid_enabled = (
"Diffuse Probe Grid component enabled",
"Diffuse Probe Grid 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_DiffuseProbeGrid_AddedToEntity():
"""
Summary:
Tests the Diffuse Probe Grid 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 Diffuse Probe Grid entity with no components.
2) Add a Diffuse Probe Grid component to Diffuse Probe Grid entity.
3) UNDO the entity creation and component addition.
4) REDO the entity creation and component addition.
5) Verify Diffuse Probe Grid component not enabled.
6) Add Shape component since it is required by the Diffuse Probe Grid component.
7) Verify Diffuse Probe Grid component is enabled.
8) Enter/Exit game mode.
9) Test IsHidden.
10) Test IsVisible.
11) Delete Diffuse Probe Grid entity.
12) UNDO deletion.
13) REDO deletion.
14) Look for errors.
:return: None
"""
import azlmbr.legacy.general as general
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Test steps begin.
# 1. Create a Diffuse Probe Grid entity with no components.
diffuse_probe_grid_entity = EditorEntity.create_editor_entity(AtomComponentProperties.diffuse_probe_grid())
Report.critical_result(Tests.diffuse_probe_grid_creation, diffuse_probe_grid_entity.exists())
# 2. Add a Diffuse Probe Grid component to Diffuse Probe Grid entity.
diffuse_probe_grid_component = diffuse_probe_grid_entity.add_component(
AtomComponentProperties.diffuse_probe_grid())
Report.critical_result(
Tests.diffuse_probe_grid_component,
diffuse_probe_grid_entity.has_component(AtomComponentProperties.diffuse_probe_grid()))
# 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 diffuse_probe_grid_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, diffuse_probe_grid_entity.exists())
# 5. Verify Diffuse Probe Grid component not enabled.
Report.result(Tests.diffuse_probe_grid_disabled, not diffuse_probe_grid_component.is_enabled())
# 6. Add Shape component since it is required by the Diffuse Probe Grid component.
for shape in AtomComponentProperties.diffuse_probe_grid('shapes'):
diffuse_probe_grid_entity.add_component(shape)
test_shape = (
f"Entity has a {shape} component",
f"Entity did not have a {shape} component")
Report.result(test_shape, diffuse_probe_grid_entity.has_component(shape))
# 7. Check if required shape allows Diffuse Probe Grid to be enabled
Report.result(Tests.diffuse_probe_grid_enabled, diffuse_probe_grid_component.is_enabled())
# Undo to remove each added shape except the last one and verify Diffuse Probe Grid is not enabled.
if not (shape == AtomComponentProperties.diffuse_probe_grid('shapes')[-1]):
general.undo()
TestHelper.wait_for_condition(lambda: not diffuse_probe_grid_entity.has_component(shape), 1.0)
Report.result(Tests.diffuse_probe_grid_disabled, not diffuse_probe_grid_component.is_enabled())
# 8. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 9. Test IsHidden.
diffuse_probe_grid_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, diffuse_probe_grid_entity.is_hidden() is True)
# 10. Test IsVisible.
diffuse_probe_grid_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, diffuse_probe_grid_entity.is_visible() is True)
# 11. Delete Diffuse Probe Grid entity.
diffuse_probe_grid_entity.delete()
Report.result(Tests.entity_deleted, not diffuse_probe_grid_entity.exists())
# 12. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, diffuse_probe_grid_entity.exists())
# 13. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not diffuse_probe_grid_entity.exists())
# 14. Look for errors or asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponents_DiffuseProbeGrid_AddedToEntity)
@@ -0,0 +1,158 @@
"""
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")
entity_reference_creation = (
"Entity Reference Entity successfully created",
"Entity Reference Entity failed to be created")
entity_reference_component = (
"Entity has an Entity Reference component",
"Entity failed to find Entity Reference component")
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_EntityReference_AddedToEntity():
"""
Summary:
Tests the Entity Reference 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 an Entity Reference entity with no components.
2) Add Entity Reference component to Entity Reference entity.
3) UNDO the entity creation and component addition.
4) REDO the entity creation and component addition.
5) Enter/Exit game mode.
6) Test IsHidden.
7) Test IsVisible.
8) Delete Entity Reference entity.
9) UNDO deletion.
10) REDO deletion.
11) Look for errors.
:return: None
"""
import azlmbr.legacy.general as general
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Test steps begin.
# 1. Create an Entity Reference entity with no components.
entity_reference_entity = EditorEntity.create_editor_entity(AtomComponentProperties.entity_reference())
Report.critical_result(Tests.entity_reference_creation, entity_reference_entity.exists())
# 2. Add Entity Reference component to Entity Reference entity.
entity_reference_component = entity_reference_entity.add_component(
AtomComponentProperties.entity_reference())
Report.critical_result(
Tests.entity_reference_component,
entity_reference_entity.has_component(AtomComponentProperties.entity_reference()))
# 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 entity_reference_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, entity_reference_entity.exists())
# 5. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 6. Test IsHidden.
entity_reference_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, entity_reference_entity.is_hidden() is True)
# 7. Test IsVisible.
entity_reference_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, entity_reference_entity.is_visible() is True)
# 8. Delete Entity Reference entity.
entity_reference_entity.delete()
Report.result(Tests.entity_deleted, not entity_reference_entity.exists())
# 9. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, entity_reference_entity.exists())
# 10. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not entity_reference_entity.exists())
# 11. Look for errors and asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponents_EntityReference_AddedToEntity)
@@ -0,0 +1,182 @@
"""
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")
ssao_creation = (
"SSAO Entity successfully created",
"SSAO Entity failed to be created")
ssao_component = (
"Entity has a SSAO component",
"Entity failed to find SSAO component")
ssao_disabled = (
"SSAO component disabled",
"SSAO component was not disabled.")
postfx_layer_component = (
"Entity has a PostFX Layer component",
"Entity did not have an PostFX Layer component")
ssao_enabled = (
"SSAO component enabled",
"SSAO 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_SSAO_AddedToEntity():
"""
Summary:
Tests the SSAO component can be added to an entity and has the expected functionality.
Screen Space Ambient Occlusion (SSAO) is a PostFX shadow lighting effect.
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 SSAO entity with no components.
2) Add SSAO component to SSAO entity.
3) UNDO the entity creation and component addition.
4) REDO the entity creation and component addition.
5) Verify SSAO component not enabled.
6) Add PostFX Layer component since it is required by the SSAO component.
7) Verify SSAO component is enabled.
8) Enter/Exit game mode.
9) Test IsHidden.
10) Test IsVisible.
11) Delete SSAO entity.
12) UNDO deletion.
13) REDO deletion.
14) Look for errors.
:return: None
"""
import azlmbr.legacy.general as general
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Test steps begin.
# 1. Create a SSAO entity with no components.
ssao_entity = EditorEntity.create_editor_entity(AtomComponentProperties.ssao())
Report.critical_result(Tests.ssao_creation, ssao_entity.exists())
# 2. Add SSAO component to SSAO entity.
ssao_component = ssao_entity.add_component(AtomComponentProperties.ssao())
Report.critical_result(
Tests.ssao_component,
ssao_entity.has_component(AtomComponentProperties.ssao()))
ssao_component.get_property_tree()
# 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 ssao_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, ssao_entity.exists())
# 5. Verify SSAO component not enabled.
Report.result(Tests.ssao_disabled, not ssao_component.is_enabled())
# 6. Add PostFX Layer component since it is required by the SSAO component.
ssao_entity.add_component(AtomComponentProperties.postfx_layer())
Report.result(
Tests.postfx_layer_component,
ssao_entity.has_component(AtomComponentProperties.postfx_layer()))
# 7. Verify SSAO component is enabled.
Report.result(Tests.ssao_enabled, ssao_component.is_enabled())
# 8. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 9. Test IsHidden.
ssao_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, ssao_entity.is_hidden() is True)
# 10. Test IsVisible.
ssao_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, ssao_entity.is_visible() is True)
# 11. Delete SSAO entity.
ssao_entity.delete()
Report.result(Tests.entity_deleted, not ssao_entity.exists())
# 12. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, ssao_entity.exists())
# 13. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not ssao_entity.exists())
# 14. Look for errors and asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponents_SSAO_AddedToEntity)
@@ -59,5 +59,8 @@ add_subdirectory(smoke)
## AWS ##
add_subdirectory(AWS)
## Multiplayer ##
add_subdirectory(Multiplayer)
## Integration tests for editor testing framework ##
add_subdirectory(editor_test_testing)
@@ -8,10 +8,10 @@
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS)
ly_add_pytest(
NAME AutomatedTesting::MultiplayerTests_Main
TEST_SUITE main
NAME AutomatedTesting::MultiplayerTests_Sandbox
TEST_SUITE sandbox
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Sandbox.py
RUNTIME_DEPENDENCIES
Legacy::Editor
AZ::AssetProcessor
@@ -27,7 +27,3 @@ class TestAutomation(TestAutomationBase):
batch_mode=batch_mode,
autotest_mode=autotest_mode)
def test_Multiplayer_AutoComponent_NetworkInput(self, request, workspace, editor, launcher_platform):
from .tests import Multiplayer_AutoComponent_NetworkInput as test_module
self._run_prefab_test(request, workspace, editor, test_module)
@@ -0,0 +1,35 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
# This suite consists of all test cases that are under development and have not been verified yet.
# Once they are verified, please move them to TestSuite_Active.py
import pytest
import os
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared')
from base import TestAutomationBase
@pytest.mark.SUITE_sandbox
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
class TestAutomation(TestAutomationBase):
def _run_prefab_test(self, request, workspace, editor, test_module, batch_mode=True, autotest_mode=True):
self._run_test(request, workspace, editor, test_module,
extra_cmdline_args=["--regset=/Amazon/Preferences/EnablePrefabSystem=true"],
batch_mode=batch_mode,
autotest_mode=autotest_mode)
## Seems to be flaky, need to investigate
def test_Multiplayer_AutoComponent_NetworkInput(self, request, workspace, editor, launcher_platform):
from .tests import Multiplayer_AutoComponent_NetworkInput as test_module
self._run_prefab_test(request, workspace, editor, test_module)
@@ -50,6 +50,7 @@ def NvCloth_AddClothSimulationToActor():
# Constants
FRAMES_IN_GAME_MODE = 200
CLOTH_GEM_ERROR_WARNING_LIST = ["Cloth", "NvCloth", "ClothComponentMesh", "ActorClothSkinning", "ActorClothSkinning", "TangentSpaceHelper", "MeshAssetHelper", "ActorAssetHelper", "ClothDebugDisplay"]
helper.init_idle()
# 1) Load the level
@@ -64,14 +65,16 @@ def NvCloth_AddClothSimulationToActor():
general.idle_wait_frames(FRAMES_IN_GAME_MODE)
# 5) Verify there are no errors and warnings in the logs
success_condition = not (section_tracer.has_errors or section_tracer.has_warnings)
Report.result(Tests.no_errors_and_warnings_found, success_condition)
if not success_condition:
if section_tracer.has_warnings:
Report.info(f"Warnings found: {section_tracer.warnings}")
if section_tracer.has_errors:
Report.info(f"Errors found: {section_tracer.errors}")
Report.failure(Tests.no_errors_and_warnings_found)
has_errors_or_warnings = False
for error_msg in section_tracer.errors:
if error_msg.window in CLOTH_GEM_ERROR_WARNING_LIST:
has_errors_or_warnings = True
Report.info(f"Cloth error found: {error_msg}")
for warning_msg in section_tracer.warnings:
if warning_msg.window in CLOTH_GEM_ERROR_WARNING_LIST:
has_errors_or_warnings = True
Report.info(f"Cloth warning found: {warning_msg}")
Report.result(Tests.no_errors_and_warnings_found, not has_errors_or_warnings)
# 6) Exit game mode
helper.exit_game_mode(Tests.exit_game_mode)
@@ -50,6 +50,7 @@ def NvCloth_AddClothSimulationToMesh():
# Constants
FRAMES_IN_GAME_MODE = 200
CLOTH_GEM_ERROR_WARNING_LIST = ["Cloth", "NvCloth", "ClothComponentMesh", "ActorClothSkinning", "ActorClothSkinning", "TangentSpaceHelper", "MeshAssetHelper", "ActorAssetHelper", "ClothDebugDisplay"]
helper.init_idle()
# 1) Load the level
@@ -64,14 +65,16 @@ def NvCloth_AddClothSimulationToMesh():
general.idle_wait_frames(FRAMES_IN_GAME_MODE)
# 5) Verify there are no errors and warnings in the logs
success_condition = not (section_tracer.has_errors or section_tracer.has_warnings)
Report.result(Tests.no_errors_and_warnings_found, success_condition)
if not success_condition:
if section_tracer.has_warnings:
Report.info(f"Warnings found: {section_tracer.warnings}")
if section_tracer.has_errors:
Report.info(f"Errors found: {section_tracer.errors}")
Report.failure(Tests.no_errors_and_warnings_found)
has_errors_or_warnings = False
for error_msg in section_tracer.errors:
if error_msg.window in CLOTH_GEM_ERROR_WARNING_LIST:
has_errors_or_warnings = True
Report.info(f"Cloth error found: {error_msg}")
for warning_msg in section_tracer.warnings:
if warning_msg.window in CLOTH_GEM_ERROR_WARNING_LIST:
has_errors_or_warnings = True
Report.info(f"Cloth warning found: {warning_msg}")
Report.result(Tests.no_errors_and_warnings_found, not has_errors_or_warnings)
# 6) Exit game mode
helper.exit_game_mode(Tests.exit_game_mode)
@@ -91,4 +91,16 @@ class TestAutomation(TestAutomationBase):
@revert_physics_config
def test_C15425929_Undo_Redo(self, request, workspace, editor, launcher_platform):
from .tests import Physics_UndoRedoWorksOnEntityWithPhysComponents as test_module
self._run_test(request, workspace, editor, test_module)
self._run_test(request, workspace, editor, test_module)
@pytest.mark.GROUP_tick
@pytest.mark.xfail(reason="Test still under development.")
def test_Tick_InterpolatedRigidBodyMotionIsSmooth(self, request, workspace, editor, launcher_platform):
from .tests.tick import Tick_InterpolatedRigidBodyMotionIsSmooth as test_module
self._run_test(request, workspace, editor, test_module)
@pytest.mark.GROUP_tick
@pytest.mark.xfail(reason="Test still under development.")
def test_Tick_CharacterGameplayComponentMotionIsSmooth(self, request, workspace, editor, launcher_platform):
from .tests.tick import Tick_CharacterGameplayComponentMotionIsSmooth as test_module
self._run_test(request, workspace, editor, test_module)
@@ -0,0 +1,99 @@
"""
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
Test Case Title : Verify that an entity with a character gameplay component moves smoothly.
"""
# fmt: off
class Tests():
create_entity = ("Created test entity", "Failed to create test entity")
character_controller_added = ("Added PhysX Character Controller component", "Failed to add PhysX Character Controller component")
character_gameplay_added = ("Added PhysX Character Gameplay component", "Failed to add PhysX Character Gameplay component")
enter_game_mode = ("Entered game mode", "Failed to enter game mode")
exit_game_mode = ("Exited game mode", "Failed to exit game mode")
character_motion_smooth = ("Character motion passed smoothness threshold", "Failed to meet smoothness threshold for character motion")
# fmt: on
def Tick_CharacterGameplayComponentMotionIsSmooth():
"""
Summary:
Create entity with PhysX Character Controller and PhysX Character Gameplay components.
Verify that the motion of the character controller under gravity is smooth.
Expected Behavior:
1) The motion of the character controller under gravity is a smooth curve, rather than an erratic/jittery movement.
Test Steps:
1) Load the empty level
2) Create an entity
3) Add a PhysX Character Controller Component and PhysX Character Gameplay component
4) Enter game mode and collect data for the character controller's z co-ordinate and the time values for a series of frames
5) Check if the motion of the character controller was sufficiently smooth
:return: None
"""
# imports
import os
import azlmbr.legacy.general as general
import azlmbr.math as math
from editor_python_test_tools.editor_entity_utils import EditorEntity as Entity
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
from editor_python_test_tools.asset_utils import Asset
import numpy as np
# constants
COEFFICIENT_OF_DETERMINATION_THRESHOLD = 1 - 1e-4 # curves with values below this are not considered sufficiently smooth
helper.init_idle()
# 1) Load the empty level
helper.open_level("", "Base")
# 2) Create an entity
test_entity = Entity.create_editor_entity("test_entity")
Report.result(Tests.create_entity, test_entity.id.IsValid())
azlmbr.components.TransformBus(
azlmbr.bus.Event, "SetWorldTranslation", test_entity.id, math.Vector3(0.0, 0.0, 0.0))
# 3) Add character controller and character gameplay components
character_controller_component = test_entity.add_component("PhysX Character Controller")
Report.result(Tests.character_controller_added, test_entity.has_component("PhysX Character Controller"))
character_gameplay_component = test_entity.add_component("PhysX Character Gameplay")
Report.result(Tests.character_gameplay_added, test_entity.has_component("PhysX Character Gameplay"))
# 4) Enter game mode and collect data for the rigid body's z co-ordinate and the time values for a series of frames
t = []
z = []
helper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
game_entity_id = general.find_game_entity("test_entity")
for frame in range(100):
t.append(azlmbr.components.TickRequestBus(azlmbr.bus.Broadcast, "GetTimeAtCurrentTick").GetSeconds())
z.append(azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldZ", game_entity_id))
general.idle_wait_frames(1)
helper.exit_game_mode(Tests.exit_game_mode)
# 5) Test that the z vs t curve is sufficiently smooth (if the interpolation is not working well, the curve will be less smooth)
# normalize the t and z data
t = np.array(t) - np.mean(t)
z = np.array(z) - np.mean(z)
# fit a polynomial to the z vs t curve
fit = np.poly1d(np.polyfit(t, z, 4))
residual = fit(t) - z
# calculate the coefficient of determination (a measure of how closely the polynomial curve fits the data)
# if the coefficient is very close to 1, then the curve fits the data very well, suggesting that the rigid body motion is smooth
# if the coefficient is significantly less than 1, then the z values vary more erratically relative to the smooth curve,
# indicating that the motion of the rigid body is not smooth
coefficient_of_determination = (1 - np.sum(residual * residual) / np.sum(z * z))
Report.result(Tests.character_motion_smooth, bool(coefficient_of_determination > COEFFICIENT_OF_DETERMINATION_THRESHOLD))
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(Tick_CharacterGameplayComponentMotionIsSmooth)
@@ -0,0 +1,98 @@
"""
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
Test Case Title : Verify that a rigid body with "Interpolate motion" option selected moves smoothly.
"""
# fmt: off
class Tests():
create_entity = ("Created test entity", "Failed to create test entity")
rigid_body_added = ("Added PhysX Rigid Body component", "Failed to add PhysX Rigid Body component")
enter_game_mode = ("Entered game mode", "Failed to enter game mode")
exit_game_mode = ("Exited game mode", "Failed to exit game mode")
rigid_body_smooth = ("Rigid body motion passed smoothness threshold", "Failed to meet smoothness threshold for rigid body motion")
# fmt: on
def Tick_InterpolatedRigidBodyMotionIsSmooth():
"""
Summary:
Create entity with PhysX Rigid Body component and turn on the Interpolate motion setting.
Verify that the position of the rigid body varies smoothly with time.
Expected Behavior:
1) The motion of the rigid body under gravity is a smooth curve, rather than an erratic/jittery movement.
Test Steps:
1) Load the empty level
2) Create an entity
3) Add rigid body component
4) Enter game mode and collect data for the rigid body's z co-ordinate and the time values for a series of frames
5) Check if the motion of the rigid body was sufficiently smooth
:return: None
"""
# imports
import os
import azlmbr.legacy.general as general
import azlmbr.math as math
from editor_python_test_tools.editor_entity_utils import EditorEntity as Entity
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
from editor_python_test_tools.asset_utils import Asset
import numpy as np
# constants
COEFFICIENT_OF_DETERMINATION_THRESHOLD = 1 - 1e-4 # curves with values below this are not considered sufficiently smooth
helper.init_idle()
# 1) Load the empty level
helper.open_level("", "Base")
# 2) Create an entity
test_entity = Entity.create_editor_entity("test_entity")
Report.result(Tests.create_entity, test_entity.id.IsValid())
azlmbr.components.TransformBus(
azlmbr.bus.Event, "SetWorldTranslation", test_entity.id, math.Vector3(0.0, 0.0, 0.0))
# 3) Add rigid body component
rigid_body_component = test_entity.add_component("PhysX Rigid Body")
rigid_body_component.set_component_property_value("Configuration|Interpolate motion", True)
azlmbr.physics.RigidBodyRequestBus(azlmbr.bus.Event, "SetLinearDamping", test_entity.id, 0.0)
Report.result(Tests.rigid_body_added, test_entity.has_component("PhysX Rigid Body"))
# 4) Enter game mode and collect data for the rigid body's z co-ordinate and the time values for a series of frames
t = []
z = []
helper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
game_entity_id = general.find_game_entity("test_entity")
for frame in range(100):
t.append(azlmbr.components.TickRequestBus(azlmbr.bus.Broadcast, "GetTimeAtCurrentTick").GetSeconds())
z.append(azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldZ", game_entity_id))
general.idle_wait_frames(1)
helper.exit_game_mode(Tests.exit_game_mode)
# 5) Test that the z vs t curve is sufficiently smooth (if the interpolation is not working well, the curve will be less smooth)
# normalize the t and z data
t = np.array(t) - np.mean(t)
z = np.array(z) - np.mean(z)
# fit a polynomial to the z vs t curve
fit = np.poly1d(np.polyfit(t, z, 4))
residual = fit(t) - z
# calculate the coefficient of determination (a measure of how closely the polynomial curve fits the data)
# if the coefficient is very close to 1, then the curve fits the data very well, suggesting that the rigid body motion is smooth
# if the coefficient is significantly less than 1, then the z values vary more erratically relative to the smooth curve,
# indicating that the motion of the rigid body is not smooth
coefficient_of_determination = (1 - np.sum(residual * residual) / np.sum(z * z))
Report.result(Tests.rigid_body_smooth, bool(coefficient_of_determination > COEFFICIENT_OF_DETERMINATION_THRESHOLD))
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(Tick_InterpolatedRigidBodyMotionIsSmooth)
@@ -158,7 +158,7 @@ def bundler_batch_setup_fixture(request, workspace, asset_processor, timeout) ->
else:
cmd.append(f"--{key}")
if append_defaults:
cmd.append(f"--project-path={workspace.project}")
cmd.append(f"--project-path={os.path.join(workspace.paths.engine_root(), workspace.project)}")
return cmd
# ******
+4 -1
View File
@@ -2,10 +2,13 @@
"gem_name": "AutomatedTesting",
"display_name": "AutomatedTesting",
"license": "Apache-2.0 Or MIT",
"license_url": "https://github.com/o3de/o3de/blob/development/LICENSE.txt",
"origin": "Amazon Web Services, Inc.",
"type": "Code",
"summary": "Project Gem for customizing the AutomatedTesting project functionality.",
"canonical_tags": ["Gem"],
"canonical_tags": [
"Gem"
],
"user_tags": [],
"icon_path": "preview.png",
"requirements": ""
+1
View File
@@ -14,6 +14,7 @@ include(cmake/Version.cmake)
include(cmake/OutputDirectory.cmake)
if(NOT PROJECT_NAME)
include(cmake/CompilerSettings.cmake)
project(O3DE
LANGUAGES C CXX
VERSION ${LY_VERSION_STRING}
+1 -30
View File
@@ -249,38 +249,9 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
RUNTIME_DEPENDENCIES
Gem::LmbrCentral
)
ly_add_googletest(
NAME Legacy::EditorLib.Tests
)
ly_add_target(
NAME EditorLib.Camera.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE Legacy
FILES_CMAKE
Lib/Tests/Camera/editor_lib_camera_test_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
.
BUILD_DEPENDENCIES
PRIVATE
AZ::AzCore
AZ::AzTest
AZ::AzToolsFramework
AZ::AzTestShared
Legacy::EditorLib
Gem::Camera.Editor
Gem::AtomToolsFramework.Static
RUNTIME_DEPENDENCIES
Legacy::EditorLib
)
ly_add_source_properties(
SOURCES Lib/Tests/Camera/test_EditorCamera.cpp
PROPERTY COMPILE_DEFINITIONS
VALUES CAMERA_EDITOR_MODULE="$<TARGET_FILE_BASE_NAME:Camera.Editor>"
)
ly_add_googletest(
NAME Legacy::EditorLib.Camera.Tests
)
endif()
+2 -1
View File
@@ -3827,7 +3827,8 @@ void CCryEditApp::OnOpenQuickAccessBar()
}
QRect geo = m_pQuickAccessBar->geometry();
geo.moveCenter(MainWindow::instance()->geometry().center());
auto mainWindow = MainWindow::instance();
geo.moveCenter(mainWindow->mapToGlobal(mainWindow->geometry().center()));
m_pQuickAccessBar->setGeometry(geo);
m_pQuickAccessBar->setVisible(true);
m_pQuickAccessBar->setFocus();
+1 -1
View File
@@ -17,7 +17,7 @@ void SetEditorEnvironment(SSystemGlobalEnvironment* pEnv)
void AttachEditorAZEnvironment(AZ::EnvironmentInstance azEnv)
{
AZ::Environment::Attach(azEnv, true);
AZ::Environment::Attach(azEnv);
}
void DetachEditorAZEnvironment()
@@ -13,9 +13,32 @@
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Render/IntersectorInterface.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h>
#include <EditorViewportSettings.h>
AZ_CVAR(
bool,
ed_cameraPinDefaultOrbit,
true,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets whether the default orbit point moves with the camera or not");
AZ_CVAR(
bool,
ed_cameraDefaultOrbitAxesOrtho,
true,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets whether to draw the default orbit point as orthographic or not");
AZ_CVAR(
float,
ed_cameraDefaultOrbitFadeDuration,
0.5f,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets how long the default orbit point should take to appear and disappear");
namespace SandboxEditor
{
static AzFramework::TranslateCameraInputChannelIds BuildTranslateCameraInputChannelIds()
@@ -174,7 +197,7 @@ namespace SandboxEditor
return SandboxEditor::CameraScrollSpeed();
};
const auto pivotFn = []
const auto pivotFn = []() -> AZStd::optional<AZ::Vector3>
{
// use the manipulator transform as the pivot point
AZStd::optional<AZ::Transform> entityPivot;
@@ -187,8 +210,7 @@ namespace SandboxEditor
return entityPivot->GetTranslation();
}
// otherwise just use the identity
return AZ::Vector3::CreateZero();
return AZStd::nullopt;
};
m_firstPersonFocusCamera =
@@ -199,9 +221,26 @@ namespace SandboxEditor
m_orbitCamera = AZStd::make_shared<AzFramework::OrbitCameraInput>(SandboxEditor::CameraOrbitChannelId());
m_orbitCamera->SetPivotFn(
[pivotFn]([[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
[this, pivotFn](const AZ::Vector3& position, const AZ::Vector3& direction)
{
return pivotFn();
// return the pivot
if (auto pivot = pivotFn())
{
return pivot.value();
}
// start ticking and drawing (for the default pivot)
AZ::TickBus::Handler::BusConnect();
AzFramework::ViewportDebugDisplayEventBus::Handler::BusConnect(AzToolsFramework::GetEntityContextId());
m_defaultOrbiting = true;
// calculate the default orbit point
if (!ed_cameraPinDefaultOrbit || m_orbitCamera->Beginning())
{
m_defaultOrbitPoint = position + direction * SandboxEditor::CameraDefaultOrbitDistance();
}
return m_defaultOrbitPoint;
});
m_orbitRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraOrbitLookChannelId());
@@ -306,4 +345,67 @@ namespace SandboxEditor
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame);
}
}
void EditorModularViewportCameraComposer::OnTick(const float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
{
const float delta = [duration = &ed_cameraDefaultOrbitFadeDuration, deltaTime] {
if (*duration == 0.0f) {
return 1.0f;
}
return deltaTime / *duration;
}();
if (m_defaultOrbiting)
{
m_defaultOrbitOpacity = AZStd::min(m_defaultOrbitOpacity + delta, 1.0f);
}
else
{
m_defaultOrbitOpacity = AZStd::max(m_defaultOrbitOpacity - delta, 0.0f);
if (m_defaultOrbitOpacity == 0.0f)
{
AZ::TickBus::Handler::BusDisconnect();
AzFramework::ViewportDebugDisplayEventBus::Handler::BusDisconnect();
}
}
m_defaultOrbiting = false;
}
static void DrawTransformAxis(
AzFramework::DebugDisplayRequests& display,
const AzFramework::CameraState& cameraState,
const AZ::Vector3& pivot,
const float axisLength,
const float alpha)
{
const int prevState = display.GetState();
display.DepthWriteOff();
display.DepthTestOff();
display.CullOff();
const float orthoScale =
ed_cameraDefaultOrbitAxesOrtho ? AzToolsFramework::CalculateScreenToWorldMultiplier(pivot, cameraState) : 1.0f;
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::Red.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisX() * axisLength * orthoScale);
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::LawnGreen.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisY() * axisLength * orthoScale);
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::Blue.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisZ() * axisLength * orthoScale);
display.DepthWriteOn();
display.DepthTestOn();
display.CullOn();
display.SetState(prevState);
}
void EditorModularViewportCameraComposer::DisplayViewport(
[[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay)
{
DrawTransformAxis(
debugDisplay, AzToolsFramework::GetCameraState(viewportInfo.m_viewportId), m_defaultOrbitPoint, 1.0f, m_defaultOrbitOpacity);
}
} // namespace SandboxEditor
@@ -9,6 +9,8 @@
#pragma once
#include <AtomToolsFramework/Viewport/ModularViewportCameraController.h>
#include <AzCore/Component/TickBus.h>
#include <AzFramework/Entity/EntityDebugDisplayBus.h>
#include <AzFramework/Viewport/CameraInput.h>
#include <AzToolsFramework/API/EditorCameraBus.h>
#include <EditorModularViewportCameraComposerBus.h>
@@ -20,6 +22,8 @@ namespace SandboxEditor
class EditorModularViewportCameraComposer
: private EditorModularViewportCameraComposerNotificationBus::Handler
, private Camera::EditorCameraNotificationBus::Handler
, private AzFramework::ViewportDebugDisplayEventBus::Handler
, private AZ::TickBus::Handler
{
public:
SANDBOX_API explicit EditorModularViewportCameraComposer(AzFramework::ViewportId viewportId);
@@ -29,6 +33,12 @@ namespace SandboxEditor
SANDBOX_API AZStd::shared_ptr<AtomToolsFramework::ModularViewportCameraController> CreateModularViewportCameraController();
private:
// AzFramework::ViewportDebugDisplayEventBus overrides ...
void DisplayViewport(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) override;
// AZ::TickBus overrides ...
void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
//! Setup all internal camera inputs.
void SetupCameras();
@@ -52,5 +62,9 @@ namespace SandboxEditor
AZStd::shared_ptr<AzFramework::FocusCameraInput> m_orbitFocusCamera;
AzFramework::ViewportId m_viewportId;
float m_defaultOrbitOpacity = 0.0f; //!< The default orbit axes opacity (to fade in and out).
AZ::Vector3 m_defaultOrbitPoint = AZ::Vector3::CreateZero(); //!< The orbit point to use when no entity is selected.
bool m_defaultOrbiting = false; //!< Is the camera default orbiting (orbiting when there's no selected entity).
};
} // namespace SandboxEditor
@@ -61,7 +61,7 @@ static AZStd::vector<AZStd::string> GetEditorInputNames()
void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serialize)
{
serialize.Class<CameraMovementSettings>()
->Version(3)
->Version(4)
->Field("TranslateSpeed", &CameraMovementSettings::m_translateSpeed)
->Field("RotateSpeed", &CameraMovementSettings::m_rotateSpeed)
->Field("BoostMultiplier", &CameraMovementSettings::m_boostMultiplier)
@@ -76,9 +76,8 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Field("OrbitYawRotationInverted", &CameraMovementSettings::m_orbitYawRotationInverted)
->Field("PanInvertedX", &CameraMovementSettings::m_panInvertedX)
->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY)
->Field("DefaultPositionX", &CameraMovementSettings::m_defaultCameraPositionX)
->Field("DefaultPositionY", &CameraMovementSettings::m_defaultCameraPositionY)
->Field("DefaultPositionZ", &CameraMovementSettings::m_defaultCameraPositionZ);
->Field("DefaultPosition", &CameraMovementSettings::m_defaultPosition)
->Field("DefaultOrbitDistance", &CameraMovementSettings::m_defaultOrbitDistance);
serialize.Class<CameraInputSettings>()
->Version(2)
@@ -159,14 +158,12 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_captureCursorLook, "Camera Capture Look Cursor",
"Should the cursor be captured (hidden) while performing free look")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionX, "Default X Camera Position",
"Default X Camera Position when a level is opened")
AZ::Edit::UIHandlers::Vector3, &CameraMovementSettings::m_defaultPosition, "Default Camera Position",
"Default Camera Position when a level is first opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionY, "Default Y Camera Position",
"Default Y Camera Position when a level is opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionZ, "Default Z Camera Position",
"Default Z Camera Position when a level is opened");
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultOrbitDistance, "Default Orbit Distance",
"The default distance to orbit about when there is no entity selected")
->Attribute(AZ::Edit::Attributes::Min, minValue);
editContext->Class<CameraInputSettings>("Camera Input Settings", "")
->DataElement(
@@ -283,12 +280,8 @@ void CEditorPreferencesPage_ViewportCamera::OnApply()
SandboxEditor::SetCameraOrbitYawRotationInverted(m_cameraMovementSettings.m_orbitYawRotationInverted);
SandboxEditor::SetCameraPanInvertedX(m_cameraMovementSettings.m_panInvertedX);
SandboxEditor::SetCameraPanInvertedY(m_cameraMovementSettings.m_panInvertedY);
SandboxEditor::SetDefaultCameraEditorPosition(
AZ::Vector3(
m_cameraMovementSettings.m_defaultCameraPositionX,
m_cameraMovementSettings.m_defaultCameraPositionY,
m_cameraMovementSettings.m_defaultCameraPositionZ
));
SandboxEditor::SetCameraDefaultEditorPosition(m_cameraMovementSettings.m_defaultPosition);
SandboxEditor::SetCameraDefaultOrbitDistance(m_cameraMovementSettings.m_defaultOrbitDistance);
SandboxEditor::SetCameraTranslateForwardChannelId(m_cameraInputSettings.m_translateForwardChannelId);
SandboxEditor::SetCameraTranslateBackwardChannelId(m_cameraInputSettings.m_translateBackwardChannelId);
@@ -325,11 +318,8 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings()
m_cameraMovementSettings.m_orbitYawRotationInverted = SandboxEditor::CameraOrbitYawRotationInverted();
m_cameraMovementSettings.m_panInvertedX = SandboxEditor::CameraPanInvertedX();
m_cameraMovementSettings.m_panInvertedY = SandboxEditor::CameraPanInvertedY();
AZ::Vector3 defaultCameraPosition = SandboxEditor::DefaultEditorCameraPosition();
m_cameraMovementSettings.m_defaultCameraPositionX = defaultCameraPosition.GetX();
m_cameraMovementSettings.m_defaultCameraPositionY = defaultCameraPosition.GetY();
m_cameraMovementSettings.m_defaultCameraPositionZ = defaultCameraPosition.GetZ();
m_cameraMovementSettings.m_defaultPosition = SandboxEditor::CameraDefaultEditorPosition();
m_cameraMovementSettings.m_defaultOrbitDistance = SandboxEditor::CameraDefaultOrbitDistance();
m_cameraInputSettings.m_translateForwardChannelId = SandboxEditor::CameraTranslateForwardChannelId().GetName();
m_cameraInputSettings.m_translateBackwardChannelId = SandboxEditor::CameraTranslateBackwardChannelId().GetName();
@@ -9,9 +9,12 @@
#pragma once
#include "Include/IPreferencesPage.h"
#include <AzCore/Math/Vector3.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <QIcon>
inline AZ::Crc32 EditorPropertyVisibility(const bool enabled)
@@ -43,6 +46,7 @@ private:
{
AZ_TYPE_INFO(CameraMovementSettings, "{60B8C07E-5F48-4171-A50B-F45558B5CCA1}")
AZ::Vector3 m_defaultPosition;
float m_translateSpeed;
float m_rotateSpeed;
float m_scrollSpeed;
@@ -50,16 +54,14 @@ private:
float m_panSpeed;
float m_boostMultiplier;
float m_rotateSmoothness;
bool m_rotateSmoothing;
float m_translateSmoothness;
bool m_translateSmoothing;
float m_defaultOrbitDistance;
bool m_captureCursorLook;
bool m_orbitYawRotationInverted;
bool m_panInvertedX;
bool m_panInvertedY;
float m_defaultCameraPositionX;
float m_defaultCameraPositionY;
float m_defaultCameraPositionZ;
bool m_rotateSmoothing;
bool m_translateSmoothing;
AZ::Crc32 RotateSmoothingVisibility() const
{
+17 -6
View File
@@ -38,6 +38,7 @@ namespace SandboxEditor
constexpr AZStd::string_view CameraTranslateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/TranslateSmoothing";
constexpr AZStd::string_view CameraRotateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/RotateSmoothing";
constexpr AZStd::string_view CameraCaptureCursorLookSetting = "/Amazon/Preferences/Editor/Camera/CaptureCursorLook";
constexpr AZStd::string_view CameraDefaultOrbitDistanceSetting = "/Amazon/Preferences/Editor/Camera/DefaultOrbitDistance";
constexpr AZStd::string_view CameraTranslateForwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateForwardId";
constexpr AZStd::string_view CameraTranslateBackwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateBackwardId";
constexpr AZStd::string_view CameraTranslateLeftIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateLeftId";
@@ -114,15 +115,15 @@ namespace SandboxEditor
return AZStd::make_unique<EditorViewportSettingsCallbacksImpl>();
}
AZ::Vector3 DefaultEditorCameraPosition()
AZ::Vector3 CameraDefaultEditorPosition()
{
float xPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionX, 0.0));
float yPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionY, -10.0));
float zPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionZ, 4.0));
return AZ::Vector3(xPosition, yPosition, zPosition);
return AZ::Vector3(
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionX, 0.0)),
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionY, -10.0)),
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionZ, 4.0)));
}
void SetDefaultCameraEditorPosition(const AZ::Vector3 defaultCameraPosition)
void SetCameraDefaultEditorPosition(const AZ::Vector3& defaultCameraPosition)
{
SetRegistry(CameraDefaultStartingPositionX, defaultCameraPosition.GetX());
SetRegistry(CameraDefaultStartingPositionY, defaultCameraPosition.GetY());
@@ -359,6 +360,16 @@ namespace SandboxEditor
SetRegistry(CameraCaptureCursorLookSetting, capture);
}
float CameraDefaultOrbitDistance()
{
return aznumeric_cast<float>(GetRegistry(CameraDefaultOrbitDistanceSetting, 20.0));
}
void SetCameraDefaultOrbitDistance(const float distance)
{
SetRegistry(CameraDefaultOrbitDistanceSetting, distance);
}
AzFramework::InputChannelId CameraTranslateForwardChannelId()
{
return AzFramework::InputChannelId(
+6 -3
View File
@@ -33,9 +33,6 @@ namespace SandboxEditor
//! event will fire when a value in the settings registry (editorpreferences.setreg) is modified.
SANDBOX_API AZStd::unique_ptr<EditorViewportSettingsCallbacks> CreateEditorViewportSettingsCallbacks();
SANDBOX_API AZ::Vector3 DefaultEditorCameraPosition();
SANDBOX_API void SetDefaultCameraEditorPosition(AZ::Vector3 defaultCameraPosition);
SANDBOX_API AZ::u64 MaxItemsShownInAssetBrowserSearch();
SANDBOX_API void SetMaxItemsShownInAssetBrowserSearch(AZ::u64 numberOfItemsShown);
@@ -105,6 +102,12 @@ namespace SandboxEditor
SANDBOX_API bool CameraCaptureCursorForLook();
SANDBOX_API void SetCameraCaptureCursorForLook(bool capture);
SANDBOX_API AZ::Vector3 CameraDefaultEditorPosition();
SANDBOX_API void SetCameraDefaultEditorPosition(const AZ::Vector3& position);
SANDBOX_API float CameraDefaultOrbitDistance();
SANDBOX_API void SetCameraDefaultOrbitDistance(float distance);
SANDBOX_API AzFramework::InputChannelId CameraTranslateForwardChannelId();
SANDBOX_API void SetCameraTranslateForwardChannelId(AZStd::string_view cameraTranslateForwardId);
+37 -11
View File
@@ -43,10 +43,11 @@
// AzToolsFramework
#include <AzToolsFramework/API/ComponentEntityObjectBus.h>
#include <AzToolsFramework/API/EditorCameraBus.h>
#include <AzToolsFramework/API/ViewportEditorModeTrackerInterface.h>
#include <AzToolsFramework/Manipulators/ManipulatorManager.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
#include <AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h>
#include <AzToolsFramework/API/EditorCameraBus.h>
// AtomToolsFramework
#include <AtomToolsFramework/Viewport/RenderViewportWidget.h>
@@ -619,9 +620,9 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
break;
case eNotify_OnEndNewScene:
PopDisableRendering();
{
PopDisableRendering();
Matrix34 viewTM;
viewTM.SetIdentity();
@@ -637,9 +638,9 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
break;
case eNotify_OnEndTerrainCreate:
PopDisableRendering();
{
PopDisableRendering();
Matrix34 viewTM;
viewTM.SetIdentity();
@@ -1032,6 +1033,7 @@ void EditorViewportWidget::ConnectViewportInteractionRequestBus()
AzToolsFramework::ViewportInteraction::MainEditorViewportInteractionRequestBus::Handler::BusConnect(GetViewportId());
AzToolsFramework::ViewportInteraction::EditorEntityViewportInteractionRequestBus::Handler::BusConnect(GetViewportId());
m_viewportUi.ConnectViewportUiBus(GetViewportId());
AzFramework::ViewportBorderRequestBus::Handler::BusConnect(GetViewportId());
AzFramework::InputSystemCursorConstraintRequestBus::Handler::BusConnect();
}
@@ -1040,6 +1042,7 @@ void EditorViewportWidget::DisconnectViewportInteractionRequestBus()
{
AzFramework::InputSystemCursorConstraintRequestBus::Handler::BusDisconnect();
AzFramework::ViewportBorderRequestBus::Handler::BusDisconnect();
m_viewportUi.DisconnectViewportUiBus();
AzToolsFramework::ViewportInteraction::EditorEntityViewportInteractionRequestBus::Handler::BusDisconnect();
AzToolsFramework::ViewportInteraction::MainEditorViewportInteractionRequestBus::Handler::BusDisconnect();
@@ -1124,7 +1127,9 @@ void EditorViewportWidget::OnTitleMenu(QMenu* menu)
action = menu->addAction(tr("Create camera entity from current view"));
connect(action, &QAction::triggered, this, &EditorViewportWidget::OnMenuCreateCameraEntityFromCurrentView);
if (!gameEngine || !gameEngine->IsLevelLoaded())
const auto prefabEditorEntityOwnershipInterface = AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
if (!gameEngine || !gameEngine->IsLevelLoaded() ||
(prefabEditorEntityOwnershipInterface && !prefabEditorEntityOwnershipInterface->IsRootPrefabAssigned()))
{
action->setEnabled(false);
action->setToolTip(tr(AZ::ViewportHelpers::TextCantCreateCameraNoLevel));
@@ -2016,10 +2021,6 @@ void EditorViewportWidget::SetDefaultCamera()
GetViewManager()->SetCameraObjectId(GUID_NULL);
SetName(m_defaultViewName);
// Set the default Editor Camera position.
m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(m_defaultViewTM);
// Synchronize the configured editor viewport FOV to the default camera
if (m_viewPane)
{
@@ -2036,6 +2037,10 @@ void EditorViewportWidget::SetDefaultCamera()
atomViewportRequests->PushView(contextName, m_defaultView);
}
// Set the default Editor Camera position.
m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(m_defaultViewTM);
PostCameraSet();
}
@@ -2522,7 +2527,7 @@ bool EditorViewportSettings::StickySelectEnabled() const
AZ::Vector3 EditorViewportSettings::DefaultEditorCameraPosition() const
{
return SandboxEditor::DefaultEditorCameraPosition();
return SandboxEditor::CameraDefaultEditorPosition();
}
AZ_CVAR_EXTERNED(bool, ed_previewGameInFullscreen_once);
@@ -2636,4 +2641,25 @@ void EditorViewportWidget::StopFullscreenPreview()
// Show the main window
MainWindow::instance()->show();
}
AZStd::optional<AzFramework::ViewportBorderPadding> EditorViewportWidget::GetViewportBorderPadding() const
{
if (auto viewportEditorModeTracker = AZ::Interface<AzToolsFramework::ViewportEditorModeTrackerInterface>::Get())
{
auto viewportEditorModes = viewportEditorModeTracker->GetViewportEditorModes({ AzToolsFramework::GetEntityContextId() });
if (viewportEditorModes->IsModeActive(AzToolsFramework::ViewportEditorMode::Focus) ||
viewportEditorModes->IsModeActive(AzToolsFramework::ViewportEditorMode::Component))
{
AzFramework::ViewportBorderPadding viewportBorderPadding = {};
viewportBorderPadding.m_top = AzToolsFramework::ViewportUi::ViewportUiTopBorderSize;
viewportBorderPadding.m_left = AzToolsFramework::ViewportUi::ViewportUiLeftRightBottomBorderSize;
viewportBorderPadding.m_right = AzToolsFramework::ViewportUi::ViewportUiLeftRightBottomBorderSize;
viewportBorderPadding.m_bottom = AzToolsFramework::ViewportUi::ViewportUiLeftRightBottomBorderSize;
return viewportBorderPadding;
}
}
return AZStd::nullopt;
}
#include <moc_EditorViewportWidget.cpp>
+5
View File
@@ -38,6 +38,7 @@
#include <AzFramework/Windowing/WindowBus.h>
#include <AzFramework/Visibility/EntityVisibilityQuery.h>
#include <AzFramework/Viewport/ViewportBus.h>
// forward declarations.
class CBaseObject;
@@ -86,6 +87,7 @@ AZ_PUSH_DISABLE_DLL_EXPORT_BASECLASS_WARNING
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
class SANDBOX_API EditorViewportWidget final
: public QtViewport
, public AzFramework::ViewportBorderRequestBus::Handler
, private IEditorNotifyListener
, private IUndoManagerListener
, private Camera::EditorCameraRequestBus::Handler
@@ -120,6 +122,9 @@ public:
void SetFOV(float fov) override;
float GetFOV() const override;
// AzFramework::ViewportBorderRequestBus overrides ...
AZStd::optional<AzFramework::ViewportBorderPadding> GetViewportBorderPadding() const override;
private:
////////////////////////////////////////////////////////////////////////
// Private types ...
+9 -20
View File
@@ -49,9 +49,6 @@
#include "Include/IObjectManager.h"
#include "ActionManager.h"
// Including this too early will result in a linker error
#include <CryCommon/CryLibrary.h>
// Implementation of System Callback structure.
struct SSystemUserCallback
: public ISystemUserCallback
@@ -242,8 +239,7 @@ private:
AZ_PUSH_DISABLE_WARNING(4273, "-Wunknown-warning-option")
CGameEngine::CGameEngine()
: m_gameDll(nullptr)
, m_bIgnoreUpdates(false)
: m_bIgnoreUpdates(false)
, m_ePendingGameMode(ePGM_NotPending)
, m_modalWindowDismisser(nullptr)
AZ_POP_DISABLE_WARNING
@@ -253,7 +249,7 @@ AZ_POP_DISABLE_WARNING
m_bInGameMode = false;
m_bSimulationMode = false;
m_bSyncPlayerPosition = true;
m_hSystemHandle = nullptr;
m_hSystemHandle.reset(nullptr);
m_bJustCreated = false;
m_levelName = "Untitled";
m_levelExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
@@ -268,18 +264,10 @@ AZ_POP_DISABLE_WARNING
GetIEditor()->UnregisterNotifyListener(this);
m_pISystem->GetIMovieSystem()->SetCallback(nullptr);
if (m_gameDll)
{
CryFreeLibrary(m_gameDll);
}
delete m_pISystem;
m_pISystem = nullptr;
if (m_hSystemHandle)
{
CryFreeLibrary(m_hSystemHandle);
}
m_hSystemHandle.reset(nullptr);
delete m_pSystemUserCallback;
}
@@ -347,18 +335,19 @@ AZ::Outcome<void, AZStd::string> CGameEngine::Init(
HWND hwndForInputSystem)
{
m_pSystemUserCallback = new SSystemUserCallback(logo);
m_hSystemHandle = CryLoadLibraryDefName("CrySystem");
if (!m_hSystemHandle)
constexpr const char* crySystemLibraryName = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX "CrySystem" AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION;
m_hSystemHandle = AZ::DynamicModuleHandle::Create(crySystemLibraryName);
if (!m_hSystemHandle->Load(true))
{
auto errorMessage = AZStd::string::format("%s Loading Failed", CryLibraryDefName("CrySystem"));
auto errorMessage = AZStd::string::format("%s Loading Failed", crySystemLibraryName);
Error(errorMessage.c_str());
return AZ::Failure(errorMessage);
}
PFNCREATESYSTEMINTERFACE pfnCreateSystemInterface =
(PFNCREATESYSTEMINTERFACE)CryGetProcAddress(m_hSystemHandle, "CreateSystemInterface");
m_hSystemHandle->GetFunction<PFNCREATESYSTEMINTERFACE>("CreateSystemInterface");
SSystemInitParams sip;
+3 -7
View File
@@ -28,6 +28,8 @@ struct IInitializeUIInfo;
#include <AzCore/Interface/Interface.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Module/DynamicModuleHandle.h>
class ThreadedOnErrorHandler : public QObject
{
Q_OBJECT
@@ -124,11 +126,6 @@ public:
return s_pakModifyMutex;
}
inline HMODULE GetGameModule() const
{
return m_gameDll;
}
private:
void SetGameMode(bool inGame);
void SwitchToInGame();
@@ -150,8 +147,7 @@ private:
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
Matrix34 m_playerViewTM;
struct SSystemUserCallback* m_pSystemUserCallback;
HMODULE m_hSystemHandle;
HMODULE m_gameDll;
AZStd::unique_ptr<AZ::DynamicModuleHandle> m_hSystemHandle;
enum EPendingGameMode
{
ePGM_NotPending,
@@ -17,43 +17,33 @@
namespace UnitTest
{
class EditorCameraTestEnvironment : public AZ::Test::GemTestEnvironment
{
// AZ::Test::GemTestEnvironment overrides ...
void AddGemsAndComponents() override;
};
void EditorCameraTestEnvironment::AddGemsAndComponents()
{
AddDynamicModulePaths({ CAMERA_EDITOR_MODULE });
AddComponentDescriptors({ AzToolsFramework::Components::TransformComponent::CreateDescriptor() });
}
class EditorCameraFixture : public ::testing::Test
{
public:
AZ::ComponentApplication* m_application = nullptr;
AtomToolsFramework::ModularCameraViewportContext* m_cameraViewportContextView = nullptr;
AZStd::unique_ptr<SandboxEditor::EditorModularViewportCameraComposer> m_editorModularViewportCameraComposer;
AZStd::unique_ptr<AZ::DynamicModuleHandle> m_editorLibHandle;
AzFramework::ViewportControllerListPtr m_controllerList;
AZStd::unique_ptr<AZ::Entity> m_entity;
AZ::Entity* m_entity = nullptr;
AZ::ComponentDescriptor* m_transformComponent = nullptr;
static const AzFramework::ViewportId TestViewportId;
void SetUp() override
{
m_editorLibHandle = AZ::DynamicModuleHandle::Create("EditorLib");
[[maybe_unused]] const bool loaded = m_editorLibHandle->Load(true);
AZ_Assert(loaded, "EditorLib could not be loaded");
m_application = aznew AZ::ComponentApplication;
AZ::ComponentApplication::Descriptor appDesc;
m_entity = m_application->Create(appDesc);
m_transformComponent = AzToolsFramework::Components::TransformComponent::CreateDescriptor();
m_application->RegisterComponentDescriptor(m_transformComponent);
m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
m_controllerList->RegisterViewportContext(TestViewportId);
m_entity = AZStd::make_unique<AZ::Entity>();
m_entity->Init();
m_entity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
m_entity->Activate();
m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
m_controllerList->RegisterViewportContext(TestViewportId);
m_editorModularViewportCameraComposer = AZStd::make_unique<SandboxEditor::EditorModularViewportCameraComposer>(TestViewportId);
auto controller = m_editorModularViewportCameraComposer->CreateModularViewportCameraController();
@@ -72,8 +62,17 @@ namespace UnitTest
{
m_editorModularViewportCameraComposer.reset();
m_cameraViewportContextView = nullptr;
m_entity.reset();
m_editorLibHandle = {};
if (m_application)
{
m_application->UnregisterComponentDescriptor(m_transformComponent);
delete m_transformComponent;
m_transformComponent = nullptr;
m_application->Destroy();
delete m_application;
m_application = nullptr;
}
}
};
@@ -211,15 +210,3 @@ namespace UnitTest
EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity()));
}
} // namespace UnitTest
// required to support running integration tests with the Camera Gem
AZTEST_EXPORT int AZ_UNIT_TEST_HOOK_NAME(int argc, char** argv)
{
::testing::InitGoogleMock(&argc, argv);
AZ::Test::printUnusedParametersWarning(argc, argv);
AZ::Test::addTestEnvironments({ new UnitTest::EditorCameraTestEnvironment() });
int result = RUN_ALL_TESTS();
return result;
}
IMPLEMENT_TEST_EXECUTABLE_MAIN();
+8 -3
View File
@@ -9,12 +9,13 @@
#include "EditorDefs.h"
#include <AzTest/AzTest.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/UnitTest/UnitTest.h>
#include <EditorEnvironment.h>
#include <QApplication>
class EditorLibTestEnvironment
: public AZ::Test::ITestEnvironment
: public ::UnitTest::TraceBusHook
{
public:
~EditorLibTestEnvironment() override = default;
@@ -22,16 +23,20 @@ public:
protected:
void SetupEnvironment() override
{
::UnitTest::TraceBusHook::SetupEnvironment();
AZ::Environment::Create(nullptr);
AttachEditorAZEnvironment(AZ::Environment::GetInstance());
AZ::AllocatorInstance<AZ::SystemAllocator>::Create();
AttachEditorAZEnvironment(AZ::Environment::GetInstance());
}
void TeardownEnvironment() override
{
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
DetachEditorAZEnvironment();
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
AZ::Environment::Destroy();
::UnitTest::TraceBusHook::TeardownEnvironment();
}
};
+1 -1
View File
@@ -44,7 +44,7 @@ namespace LyViewPane
static const char* const SubstanceEditor = "Substance Editor";
static const char* const VegetationEditor = "Vegetation Editor";
static const char* const LandscapeCanvas = "Landscape Canvas";
static const char* const AnimationEditor = "EMotion FX Animation Editor (PREVIEW)";
static const char* const AnimationEditor = "EMotion FX Animation Editor";
static const char* const PhysXConfigurationEditor = "PhysX Configuration (PREVIEW)";
static const char* const SliceRelationships = "Slice Relationship View (PREVIEW)";
-1
View File
@@ -27,7 +27,6 @@
// Editor
#include "MainStatusBarItems.h"
#include "CryLibrary.h"
#include "ProcessInfo.h"
+1 -1
View File
@@ -1061,7 +1061,7 @@ void MainWindow::InitActions()
if (emfxEnabled.value)
{
QAction* action = am->AddAction(ID_OPEN_EMOTIONFX_EDITOR, tr("Animation Editor"))
.SetToolTip(tr("Open Animation Editor (PREVIEW)"))
.SetToolTip(tr("Open Animation Editor"))
.SetIcon(QIcon(":/EMotionFX/EMFX_icon_32x32.png"))
.SetApplyHoverEffect();
QObject::connect(action, &QAction::triggered, this, []() {
@@ -10,6 +10,8 @@
#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
#include <AzFramework/XcbEventHandler.h>
#include <AzFramework/XcbConnectionManager.h>
#include <qpa/qplatformnativeinterface.h>
#endif
namespace Editor
@@ -23,16 +25,34 @@ namespace Editor
return nullptr;
}
xcb_connection_t* EditorQtApplicationXcb::GetXcbConnectionFromQt()
{
QPlatformNativeInterface* native = platformNativeInterface();
AZ_Warning("EditorQtApplicationXcb", native, "Unable to retrieve the native platform interface");
if (!native)
{
return nullptr;
}
return reinterpret_cast<xcb_connection_t*>(native->nativeResourceForIntegration(QByteArray("connection")));
}
void EditorQtApplicationXcb::OnStartPlayInEditor()
{
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
interface->SetEnableXInput(GetXcbConnectionFromQt(), true);
}
void EditorQtApplicationXcb::OnStopPlayInEditor()
{
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
interface->SetEnableXInput(GetXcbConnectionFromQt(), false);
}
bool EditorQtApplicationXcb::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long*)
{
if (GetIEditor()->IsInGameMode())
{
#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
// We need to handle RAW Input events in a separate loop. This is a workaround to enable XInput2 RAW Inputs using Editor mode.
// TODO To have this call here might be not be perfect.
AzFramework::XcbEventHandlerBus::Broadcast(&AzFramework::XcbEventHandler::PollSpecialEvents);
// Now handle the rest of the events.
AzFramework::XcbEventHandlerBus::Broadcast(
&AzFramework::XcbEventHandler::HandleXcbEvent, static_cast<xcb_generic_event_t*>(message));
#endif
@@ -6,19 +6,35 @@
*
*/
#if !defined(Q_MOC_RUN)
#include <Editor/Core/QtEditorApplication.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#endif
using xcb_connection_t = struct xcb_connection_t;
namespace Editor
{
class EditorQtApplicationXcb : public EditorQtApplication
class EditorQtApplicationXcb
: public EditorQtApplication
, public AzToolsFramework::EditorEntityContextNotificationBus::Handler
{
Q_OBJECT
public:
EditorQtApplicationXcb(int& argc, char** argv)
: EditorQtApplication(argc, argv)
{
// Connect bus to listen for OnStart/StopPlayInEditor events
AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusConnect();
}
xcb_connection_t* GetXcbConnectionFromQt();
///////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorEntityContextNotificationBus overrides
void OnStartPlayInEditor() override;
void OnStopPlayInEditor() override;
// QAbstractNativeEventFilter:
bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override;
};
+1
View File
@@ -22,6 +22,7 @@ set(FILES
Lib/Tests/test_DisplaySettingsPythonBindings.cpp
Lib/Tests/test_ViewportManipulatorController.cpp
Lib/Tests/test_ModularViewportCameraController.cpp
Lib/Tests/Camera/test_EditorCamera.cpp
DisplaySettingsPythonFuncs.cpp
DisplaySettingsPythonFuncs.h
)
@@ -70,6 +70,9 @@ namespace AZ::Data
const char* GetFilename() const override { return m_filePath.c_str(); }
AZStd::chrono::milliseconds GetStreamingDeadline() const { return m_curDeadline; }
AZ::IO::IStreamerTypes::Priority GetStreamingPriority() const { return m_curPriority; }
// AssetDataStream specific APIs
//! Whether or not all data has been loaded.
@@ -214,7 +214,6 @@ namespace AZ
// Update old Project path before attempting to merge in new Settings Registry values in order to prevent recursive calls
m_oldProjectPath = newProjectPath;
// Merge the project.json file into settings registry under ProjectSettingsRootKey path.
// Update all the runtime file paths based on the new "project_path" value.
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(m_registry);
}
@@ -457,8 +457,6 @@ void JobManagerWorkStealing::ProcessJobsInternal(ThreadInfo* info, Job* suspende
else
{
//attempt to steal a job from another thread's queue
AZ_PROFILE_SCOPE(AzCore, "JobManagerWorkStealing::ProcessJobsInternal:WorkStealing");
unsigned int numStealAttempts = 0;
const unsigned int maxStealAttempts = (unsigned int)m_workerThreads.size() * 3; //try every thread a few times before giving up
while (!job)
@@ -3408,7 +3408,14 @@ LUA_API const Node* lua_getDummyNode()
const BehaviorParameter* arg = method->GetArgument(iArg);
BehaviorClass* argClass = nullptr;
LuaLoadFromStack fromStack = FromLuaStack(context, arg, argClass);
AZ_Assert(fromStack, "Argument %s for Method %s doesn't have support to be converted to Lua!", arg->m_name, method->m_name.c_str());
AZ_Assert(fromStack,
"The argument type: %s for method: %s is not serialized and/or reflected for scripting.\n"
"Make sure %s is added to the SerializeContext and reflected to the BehaviorContext\n"
"For example, verify these two exist and are being called in a Reflect function:\n"
"serializeContext->Class<%s>();\n"
"behaviorContext->Class<%s>();\n"
"%s will not be available for scripting unless these requirements are met."
, arg->m_name, method->m_name.c_str(), arg->m_name, arg->m_name, arg->m_name, method->m_name.c_str());
m_fromLua.push_back(AZStd::make_pair(fromStack, argClass));
}
@@ -29,6 +29,8 @@
namespace AZ::Internal
{
static constexpr const char* ProductCacheDirectoryName = "Cache";
AZ::SettingsRegistryInterface::FixedValueString GetEngineMonikerForProject(
SettingsRegistryInterface& settingsRegistry, const AZ::IO::FixedMaxPath& projectJsonPath)
{
@@ -228,19 +230,20 @@ namespace AZ::Internal
namespace AZ::SettingsRegistryMergeUtils
{
constexpr AZStd::string_view InternalScanUpEngineRootKey{ "/O3DE/Settings/Internal/engine_root_scan_up_path" };
constexpr AZStd::string_view InternalScanUpProjectRootKey{ "/O3DE/Settings/Internal/project_root_scan_up_path" };
AZ::IO::FixedMaxPath FindEngineRoot(SettingsRegistryInterface& settingsRegistry)
{
static constexpr AZStd::string_view InternalScanUpEngineRootKey{ "/O3DE/Runtime/Internal/engine_root_scan_up_path" };
using FixedValueString = SettingsRegistryInterface::FixedValueString;
using Type = SettingsRegistryInterface::Type;
AZ::IO::FixedMaxPath engineRoot;
// This is the 'external' engine root key, as in passed from command-line or .setreg files.
auto engineRootKey = SettingsRegistryInterface::FixedValueString::format("%s/engine_path", BootstrapSettingsRootKey);
constexpr auto engineRootKey = FixedValueString(BootstrapSettingsRootKey) + "/engine_path";
// Step 1 Run the scan upwards logic once to find the location of the engine.json if it exist
// Once this step is run the {InternalScanUpEngineRootKey} is set in the Settings Registry
// to have this scan logic only run once InternalScanUpEngineRootKey the supplied registry
if (settingsRegistry.GetType(InternalScanUpEngineRootKey) == SettingsRegistryInterface::Type::NoType)
if (settingsRegistry.GetType(InternalScanUpEngineRootKey) == Type::NoType)
{
// We can scan up from exe directory to find engine.json, use that for engine root if it exists.
engineRoot = Internal::ScanUpRootLocator("engine.json");
@@ -283,14 +286,18 @@ namespace AZ::SettingsRegistryMergeUtils
AZ::IO::FixedMaxPath FindProjectRoot(SettingsRegistryInterface& settingsRegistry)
{
AZ::IO::FixedMaxPath projectRoot;
const auto projectRootKey = SettingsRegistryInterface::FixedValueString::format("%s/project_path", BootstrapSettingsRootKey);
static constexpr AZStd::string_view InternalScanUpProjectRootKey{ "/O3DE/Runtime/Internal/project_root_scan_up_path" };
using FixedValueString = SettingsRegistryInterface::FixedValueString;
using Type = SettingsRegistryInterface::Type;
// Step 1 Run the scan upwards logic once to find the location of the project.json if it exist
AZ::IO::FixedMaxPath projectRoot;
constexpr auto projectRootKey = FixedValueString(BootstrapSettingsRootKey) + "/project_path";
// Step 1 Run the scan upwards logic once to find the location of the closest ancestor project.json
// Once this step is run the {InternalScanUpProjectRootKey} is set in the Settings Registry
// to have this scan logic only run once for the supplied registry
// SettingsRegistryInterface::GetType is used to check if a key is set
if (settingsRegistry.GetType(InternalScanUpProjectRootKey) == SettingsRegistryInterface::Type::NoType)
if (settingsRegistry.GetType(InternalScanUpProjectRootKey) == Type::NoType)
{
projectRoot = Internal::ScanUpRootLocator("project.json");
// Set the {InternalScanUpProjectRootKey} to make sure this code path isn't called again for this settings registry
@@ -305,19 +312,129 @@ namespace AZ::SettingsRegistryMergeUtils
}
// Step 2 Check the project-path key
// This is the project path root key, as in passed from command-line or .setreg files.
if (settingsRegistry.Get(projectRoot.Native(), projectRootKey))
// This is the project path root key, as passed from command-line or *.setreg files.
settingsRegistry.Get(projectRoot.Native(), projectRootKey);
return projectRoot;
}
//! The algorithm that is used to find the project cache is as follows
//! 1. The "{BootstrapSettingsRootKey}/project_cache_path" is checked for the path
//! 2. Otherwise append the ProductCacheDirectoryName constant to the <project-path>
static AZ::IO::FixedMaxPath FindProjectCachePath(SettingsRegistryInterface& settingsRegistry, const AZ::IO::FixedMaxPath& projectPath)
{
using FixedValueString = SettingsRegistryInterface::FixedValueString;
constexpr auto projectCachePathKey = FixedValueString(BootstrapSettingsRootKey) + "/project_cache_path";
// Step 1 Check the project-cache-path key
if (AZ::IO::FixedMaxPath projectCachePath; settingsRegistry.Get(projectCachePath.Native(), projectCachePathKey))
{
return projectRoot;
return projectCachePath;
}
// Step 3 Check for a "Cache" directory by scanning upwards from the executable directory
if (auto candidateRoot = Internal::ScanUpRootLocator("Cache");
!candidateRoot.empty() && AZ::IO::SystemFile::IsDirectory(candidateRoot.c_str()))
// Step 2 Append the "Cache" directory to the project-path
return projectPath / Internal::ProductCacheDirectoryName;
}
//! Set the user directory with the provided path or using <project-path>/user as default
static AZ::IO::FixedMaxPath FindProjectUserPath(SettingsRegistryInterface& settingsRegistry,
const AZ::IO::FixedMaxPath& projectPath)
{
using FixedValueString = SettingsRegistryInterface::FixedValueString;
// User: root - same as the @user@ alias, this is the starting path for transient data and log files.
constexpr auto projectUserPathKey = FixedValueString(BootstrapSettingsRootKey) + "/project_user_path";
// Step 1 Check the project-user-path key
if (AZ::IO::FixedMaxPath projectUserPath; settingsRegistry.Get(projectUserPath.Native(), projectUserPathKey))
{
projectRoot = AZStd::move(candidateRoot);
return projectUserPath;
}
// Step 2 Append the "User" directory to the project-path
return projectPath / "user";
}
//! Set the log directory using the settings registry path or using <project-user-path>/log as default
static AZ::IO::FixedMaxPath FindProjectLogPath(SettingsRegistryInterface& settingsRegistry,
const AZ::IO::FixedMaxPath& projectUserPath)
{
using FixedValueString = SettingsRegistryInterface::FixedValueString;
// User: root - same as the @log@ alias, this is the starting path for transient data and log files.
constexpr auto projectLogPathKey = FixedValueString(BootstrapSettingsRootKey) + "/project_log_path";
// Step 1 Check the project-user-path key
if (AZ::IO::FixedMaxPath projectLogPath; settingsRegistry.Get(projectLogPath.Native(), projectLogPathKey))
{
return projectLogPath;
}
// Step 2 Append the "Log" directory to the project-user-path
return projectUserPath / "log";
}
// check for a default write storage path, fall back to the <project-user-path> if not
static AZ::IO::FixedMaxPath FindDevWriteStoragePath(const AZ::IO::FixedMaxPath& projectUserPath)
{
AZStd::optional<AZ::IO::FixedMaxPathString> devWriteStorage = Utils::GetDevWriteStoragePath();
return devWriteStorage.has_value() ? *devWriteStorage : projectUserPath;
}
// check for the project build path, which is a relative path from the project root
// that specifies where the build directory is located
static void SetProjectBuildPath(SettingsRegistryInterface& settingsRegistry,
const AZ::IO::FixedMaxPath& projectPath)
{
if (AZ::IO::FixedMaxPath projectBuildPath; settingsRegistry.Get(projectBuildPath.Native(), ProjectBuildPath))
{
settingsRegistry.Remove(FilePathKey_ProjectBuildPath);
settingsRegistry.Remove(FilePathKey_ProjectConfigurationBinPath);
AZ::IO::FixedMaxPath buildConfigurationPath = (projectPath / projectBuildPath).LexicallyNormal();
if (IO::SystemFile::Exists(buildConfigurationPath.c_str()))
{
settingsRegistry.Set(FilePathKey_ProjectBuildPath, buildConfigurationPath.Native());
}
// Add the specific build configuration paths to the Settings Registry
// First try <project-build-path>/bin/$<CONFIG> and if that path doesn't exist
// try <project-build-path>/bin/$<PLATFORM>/$<CONFIG>
buildConfigurationPath /= "bin";
if (IO::SystemFile::Exists((buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE).c_str()))
{
settingsRegistry.Set(FilePathKey_ProjectConfigurationBinPath,
(buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE).Native());
}
else if (IO::SystemFile::Exists((buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE).c_str()))
{
settingsRegistry.Set(FilePathKey_ProjectConfigurationBinPath,
(buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE).Native());
}
}
}
// Sets the project name within the Settings Registry by looking up the "project_name"
// within the project.json file
static void SetProjectName(SettingsRegistryInterface& settingsRegistry,
const AZ::IO::FixedMaxPath& projectPath)
{
using FixedValueString = SettingsRegistryInterface::FixedValueString;
// Project name - if it was set via merging project.json use that value, otherwise use the project path's folder name.
constexpr auto projectNameKey = FixedValueString(ProjectSettingsRootKey) + "/project_name";
// Read the project name from the project.json file if it exists
if (AZ::IO::FixedMaxPath projectJsonPath = projectPath / "project.json";
AZ::IO::SystemFile::Exists(projectJsonPath.c_str()))
{
settingsRegistry.MergeSettingsFile(projectJsonPath.Native(),
AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey);
}
// If a project name isn't set the default will be set to the final path segment of the project path
if (FixedValueString projectName; !settingsRegistry.Get(projectName, projectNameKey))
{
projectName = projectPath.Filename().Native();
settingsRegistry.Set(projectNameKey, projectName);
}
return projectRoot;
}
AZStd::string_view ConfigParserSettings::DefaultCommentPrefixFilter(AZStd::string_view line)
@@ -397,7 +514,7 @@ namespace AZ::SettingsRegistryMergeUtils
bool MergeSettingsToRegistry_ConfigFile(SettingsRegistryInterface& registry, AZStd::string_view filePath,
const ConfigParserSettings& configParserSettings)
{
auto configPath = FindEngineRoot(registry) / filePath;
auto configPath = FindProjectRoot(registry) / filePath;
IO::FileReader configFile;
bool configFileOpened{};
switch (configParserSettings.m_fileReaderClass)
@@ -542,19 +659,77 @@ namespace AZ::SettingsRegistryMergeUtils
void MergeSettingsToRegistry_AddRuntimeFilePaths(SettingsRegistryInterface& registry)
{
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
// Binary folder
AZ::IO::FixedMaxPath path = AZ::Utils::GetExecutableDirectory();
registry.Set(FilePathKey_BinaryFolder, path.LexicallyNormal().Native());
// Engine root folder - corresponds to the @engroot@ and @engroot@ aliases
AZ::IO::FixedMaxPath engineRoot = FindEngineRoot(registry);
registry.Set(FilePathKey_EngineRootFolder, engineRoot.LexicallyNormal().Native());
// Binary folder - corresponds to the @exefolder@ alias
AZ::IO::FixedMaxPath exePath = AZ::Utils::GetExecutableDirectory();
registry.Set(FilePathKey_BinaryFolder, exePath.LexicallyNormal().Native());
auto projectPathKey = FixedValueString::format("%s/project_path", BootstrapSettingsRootKey);
SettingsRegistryInterface::FixedValueString projectPathValue;
if (registry.Get(projectPathValue, projectPathKey))
// Project path - corresponds to the @projectroot@ alias
// NOTE: We make the project-path in the BootstrapSettingsRootKey absolute first
AZ::IO::FixedMaxPath projectPath = FindProjectRoot(registry);
if (constexpr auto projectPathKey = FixedValueString(BootstrapSettingsRootKey) + "/project_path";
!projectPath.empty())
{
// Cache folder
if (projectPath.IsRelative())
{
if (auto projectAbsPath = AZ::Utils::ConvertToAbsolutePath(projectPath.Native());
projectAbsPath.has_value())
{
projectPath = AZStd::move(*projectAbsPath);
}
}
projectPath = projectPath.LexicallyNormal();
AZ_Warning("SettingsRegistryMergeUtils", AZ::IO::SystemFile::Exists(projectPath.c_str()),
R"(Project path "%s" does not exist. Is the "%.*s" registry setting set to a valid absolute path?)"
, projectPath.c_str(), AZ_STRING_ARG(projectPathKey));
registry.Set(FilePathKey_ProjectPath, projectPath.Native());
}
else
{
AZ_TracePrintf("SettingsRegistryMergeUtils",
R"(Project path isn't set in the Settings Registry at "%.*s".)"
" Project-related filepaths will be set relative to the executable directory\n",
AZ_STRING_ARG(projectPathKey));
registry.Set(FilePathKey_ProjectPath, exePath.Native());
}
// Engine root folder - corresponds to the @engroot@ alias
AZ::IO::FixedMaxPath engineRoot = FindEngineRoot(registry);
if (!engineRoot.empty())
{
if (engineRoot.IsRelative())
{
if (auto engineRootAbsPath = AZ::Utils::ConvertToAbsolutePath(engineRoot.Native());
engineRootAbsPath.has_value())
{
engineRoot = AZStd::move(*engineRootAbsPath);
}
}
engineRoot = engineRoot.LexicallyNormal();
registry.Set(FilePathKey_EngineRootFolder, engineRoot.Native());
}
// Cache folder
AZ::IO::FixedMaxPath projectCachePath = FindProjectCachePath(registry, projectPath).LexicallyNormal();
if (!projectCachePath.empty())
{
if (projectCachePath.IsRelative())
{
if (auto projectCacheAbsPath = AZ::Utils::ConvertToAbsolutePath(projectCachePath.Native());
projectCacheAbsPath.has_value())
{
projectCachePath = AZStd::move(*projectCacheAbsPath);
}
}
projectCachePath = projectCachePath.LexicallyNormal();
registry.Set(FilePathKey_CacheProjectRootFolder, projectCachePath.Native());
// Cache/<asset-platform> folder
// Get the name of the asset platform assigned by the bootstrap. First check for platform version such as "windows_assets"
// and if that's missing just get "assets".
FixedValueString assetPlatform;
@@ -570,124 +745,67 @@ namespace AZ::SettingsRegistryMergeUtils
assetPlatform = AZ::OSPlatformToDefaultAssetPlatform(AZ_TRAIT_OS_PLATFORM_CODENAME);
}
// Project path - corresponds to the @projectroot@ alias
// NOTE: Here we append to engineRoot, but if projectPathValue is absolute then engineRoot is discarded.
path = engineRoot / projectPathValue;
AZ_Warning("SettingsRegistryMergeUtils", AZ::IO::SystemFile::Exists(path.c_str()),
R"(Project path "%s" does not exist. Is the "%.*s" registry setting set to valid absolute path?)"
, path.c_str(), aznumeric_cast<int>(projectPathKey.size()), projectPathKey.data());
AZ::IO::FixedMaxPath normalizedProjectPath = path.LexicallyNormal();
registry.Set(FilePathKey_ProjectPath, normalizedProjectPath.Native());
// Set the user directory with the provided path or using project/user as default
auto projectUserPathKey = FixedValueString::format("%s/project_user_path", BootstrapSettingsRootKey);
AZ::IO::FixedMaxPath projectUserPath;
if (!registry.Get(projectUserPath.Native(), projectUserPathKey))
{
projectUserPath = (normalizedProjectPath / "user").LexicallyNormal();
}
registry.Set(FilePathKey_ProjectUserPath, projectUserPath.Native());
// Set the log directory with the provided path or using project/user/log as default
auto projectLogPathKey = FixedValueString::format("%s/project_log_path", BootstrapSettingsRootKey);
AZ::IO::FixedMaxPath projectLogPath;
if (!registry.Get(projectLogPath.Native(), projectLogPathKey))
{
projectLogPath = (projectUserPath / "log").LexicallyNormal();
}
registry.Set(FilePathKey_ProjectLogPath, projectLogPath.Native());
// check for a default write storage path, fall back to the project's user/ directory if not
AZStd::optional<AZ::IO::FixedMaxPathString> devWriteStorage = Utils::GetDevWriteStoragePath();
registry.Set(FilePathKey_DevWriteStorage, devWriteStorage.has_value()
? devWriteStorage.value()
: projectUserPath.Native());
// Set the project in-memory build path if the ProjectBuildPath key has been supplied
if (AZ::IO::FixedMaxPath projectBuildPath; registry.Get(projectBuildPath.Native(), ProjectBuildPath))
{
registry.Remove(FilePathKey_ProjectBuildPath);
registry.Remove(FilePathKey_ProjectConfigurationBinPath);
AZ::IO::FixedMaxPath buildConfigurationPath = normalizedProjectPath / projectBuildPath;
if (IO::SystemFile::Exists(buildConfigurationPath.c_str()))
{
registry.Set(FilePathKey_ProjectBuildPath, buildConfigurationPath.LexicallyNormal().Native());
}
// Add the specific build configuration paths to the Settings Registry
// First try <project-build-path>/bin/$<CONFIG> and if that path doesn't exist
// try <project-build-path>/bin/$<PLATFORM>/$<CONFIG>
buildConfigurationPath /= "bin";
if (IO::SystemFile::Exists((buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE).c_str()))
{
registry.Set(FilePathKey_ProjectConfigurationBinPath,
(buildConfigurationPath / AZ_BUILD_CONFIGURATION_TYPE).LexicallyNormal().Native());
}
else if (IO::SystemFile::Exists((buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE).c_str()))
{
registry.Set(FilePathKey_ProjectConfigurationBinPath,
(buildConfigurationPath / AZ_TRAIT_OS_PLATFORM_CODENAME / AZ_BUILD_CONFIGURATION_TYPE).LexicallyNormal().Native());
}
}
// Project name - if it was set via merging project.json use that value, otherwise use the project path's folder name.
constexpr auto projectNameKey =
FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey)
+ "/project_name";
// Read the project name from the project.json file if it exists
if (AZ::IO::FixedMaxPath projectJsonPath = normalizedProjectPath / "project.json";
AZ::IO::SystemFile::Exists(projectJsonPath.c_str()))
{
registry.MergeSettingsFile(projectJsonPath.Native(),
AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey);
}
if (FixedValueString projectName; !registry.Get(projectName, projectNameKey))
{
projectName = path.Filename().Native();
registry.Set(projectNameKey, projectName);
}
// Cache folders - sets up various paths in registry for the cache.
// Make sure the asset platform is set before setting these cache paths.
// Make sure the asset platform is set before setting cache path for the asset platform.
if (!assetPlatform.empty())
{
// Cache: project root - no corresponding fileIO alias, but this is where the asset database lives.
// A registry override is accepted using the "project_cache_path" key.
auto projectCacheRootOverrideKey = FixedValueString::format("%s/project_cache_path", BootstrapSettingsRootKey);
// Clear path to make sure that the `project_cache_path` value isn't concatenated to the project path
path.clear();
if (registry.Get(path.Native(), projectCacheRootOverrideKey))
{
registry.Set(FilePathKey_CacheProjectRootFolder, path.LexicallyNormal().Native());
path /= assetPlatform;
registry.Set(FilePathKey_CacheRootFolder, path.LexicallyNormal().Native());
}
else
{
// Cache: root - same as the @products@ alias, this is the starting path for cache files.
path = normalizedProjectPath / "Cache";
registry.Set(FilePathKey_CacheProjectRootFolder, path.LexicallyNormal().Native());
path /= assetPlatform;
registry.Set(FilePathKey_CacheRootFolder, path.LexicallyNormal().Native());
}
registry.Set(FilePathKey_CacheRootFolder, (projectCachePath / assetPlatform).Native());
}
}
else
// User folder
AZ::IO::FixedMaxPath projectUserPath = FindProjectUserPath(registry, projectPath);
if (!projectUserPath.empty())
{
// Set the default ProjectUserPath to the <engine-root>/user directory
registry.Set(FilePathKey_ProjectUserPath, (engineRoot / "user").LexicallyNormal().Native());
AZ_TracePrintf("SettingsRegistryMergeUtils",
R"(Project path isn't set in the Settings Registry at "%.*s". Project-related filepaths will not be set)" "\n",
aznumeric_cast<int>(projectPathKey.size()), projectPathKey.data());
if (projectUserPath.IsRelative())
{
if (auto projectUserAbsPath = AZ::Utils::ConvertToAbsolutePath(projectUserPath.Native());
projectUserAbsPath.has_value())
{
projectUserPath = AZStd::move(*projectUserAbsPath);
}
}
projectUserPath = projectUserPath.LexicallyNormal();
registry.Set(FilePathKey_ProjectUserPath, projectUserPath.Native());
}
// Log folder
if (AZ::IO::FixedMaxPath projectLogPath = FindProjectLogPath(registry, projectUserPath); !projectLogPath.empty())
{
if (projectLogPath.IsRelative())
{
if (auto projectLogAbsPath = AZ::Utils::ConvertToAbsolutePath(projectLogPath.Native()))
{
projectLogPath = AZStd::move(*projectLogAbsPath);
}
}
projectLogPath = projectLogPath.LexicallyNormal();
registry.Set(FilePathKey_ProjectLogPath, projectLogPath.Native());
}
// Developer Write Storage folder
if (AZ::IO::FixedMaxPath devWriteStoragePath = FindDevWriteStoragePath(projectUserPath); !devWriteStoragePath.empty())
{
if (devWriteStoragePath.IsRelative())
{
if (auto devWriteStorageAbsPath = AZ::Utils::ConvertToAbsolutePath(devWriteStoragePath.Native()))
{
devWriteStoragePath = AZStd::move(*devWriteStorageAbsPath);
}
}
devWriteStoragePath = devWriteStoragePath.LexicallyNormal();
registry.Set(FilePathKey_DevWriteStorage, devWriteStoragePath.Native());
}
// Set the project in-memory build path if the ProjectBuildPath key has been supplied
SetProjectBuildPath(registry, projectPath);
// Set the project name using the "project_name" key
SetProjectName(registry, projectPath);
#if !AZ_TRAIT_OS_IS_HOST_OS_PLATFORM
// Setup the cache, user, and log paths to platform specific locations when running on non-host platforms
path = engineRoot;
if (AZStd::optional<AZ::IO::FixedMaxPathString> nonHostCacheRoot = Utils::GetDefaultAppRootPath();
nonHostCacheRoot)
{
@@ -696,25 +814,25 @@ namespace AZ::SettingsRegistryMergeUtils
}
else
{
registry.Set(FilePathKey_CacheProjectRootFolder, path.LexicallyNormal().Native());
registry.Set(FilePathKey_CacheRootFolder, path.LexicallyNormal().Native());
registry.Set(FilePathKey_CacheProjectRootFolder, projectPath.Native());
registry.Set(FilePathKey_CacheRootFolder, projectPath.Native());
}
if (AZStd::optional<AZ::IO::FixedMaxPathString> devWriteStorage = Utils::GetDevWriteStoragePath();
devWriteStorage)
{
const AZ::IO::FixedMaxPath devWriteStoragePath(*devWriteStorage);
registry.Set(FilePathKey_DevWriteStorage, devWriteStoragePath.LexicallyNormal().Native());
registry.Set(FilePathKey_ProjectUserPath, (devWriteStoragePath / "user").LexicallyNormal().Native());
registry.Set(FilePathKey_ProjectLogPath, (devWriteStoragePath / "user/log").LexicallyNormal().Native());
const auto devWriteStoragePath = AZ::IO::PathView(*devWriteStorage).LexicallyNormal();
registry.Set(FilePathKey_DevWriteStorage, devWriteStoragePath.Native());
registry.Set(FilePathKey_ProjectUserPath, (devWriteStoragePath / "user").Native());
registry.Set(FilePathKey_ProjectLogPath, (devWriteStoragePath / "user" / "log").Native());
}
else
{
registry.Set(FilePathKey_DevWriteStorage, path.LexicallyNormal().Native());
registry.Set(FilePathKey_ProjectUserPath, (path / "user").LexicallyNormal().Native());
registry.Set(FilePathKey_ProjectLogPath, (path / "user/log").LexicallyNormal().Native());
}
#endif // AZ_TRAIT_OS_IS_HOST_OS_PLATFORM
registry.Set(FilePathKey_DevWriteStorage, projectPath.Native());
registry.Set(FilePathKey_ProjectUserPath, (projectPath / "user").Native());
registry.Set(FilePathKey_ProjectLogPath, (projectPath / "user" / "log").Native());
}
#endif // AZ_TRAIT_OS_IS_HOST_OS_PLATFORM
}
void MergeSettingsToRegistry_TargetBuildDependencyRegistry(SettingsRegistryInterface& registry, const AZStd::string_view platform,
const SettingsRegistryInterface::Specializations& specializations, AZStd::vector<char>* scratchBuffer)
@@ -87,9 +87,9 @@ namespace AZ::SettingsRegistryMergeUtils
AZ::IO::FixedMaxPath FindEngineRoot(SettingsRegistryInterface& settingsRegistry);
//! The algorithm that is used to find the project root is as follows
//! 1. The first time this function is it performs a upward scan for a project.json file from
//! the executable directory and if found stores that path to an internal key.
//! In the same step it injects the path into the front of list of command line parameters
//! 1. The first time this function runs it performs an upward scan for a "project.json" file from
//! the executable directory and stores that path into an internal key.
//! In the same step it injects the path into the back of the command line parameters
//! using the --regset="{BootstrapSettingsRootKey}/project_path=<path>" value
//! 2. Next the "{BootstrapSettingsRootKey}/project_path" is checked to see if it has a project path set
//!
+1 -1
View File
@@ -59,7 +59,7 @@ namespace AZ::Utils
{
// Fix the size value of the fixed string by calculating the c-string length using char traits
absolutePath.resize_no_construct(AZStd::char_traits<char>::length(absolutePath.data()));
return srcPath;
return absolutePath;
}
return AZStd::nullopt;
@@ -635,6 +635,7 @@ namespace AZ
size_t longestMatch = 0;
size_t bufStringLength = inBuffer.size();
AZStd::string_view longestAlias;
AZStd::string_view longestResolvedAlias;
for (const auto& [alias, resolvedAlias] : m_aliases)
{
@@ -653,6 +654,7 @@ namespace AZ
{
longestMatch = resolvedAlias.size();
longestAlias = alias;
longestResolvedAlias = resolvedAlias;
}
}
}
@@ -661,7 +663,10 @@ namespace AZ
// rearrange the buffer to have
// [alias][old path]
size_t aliasSize = longestAlias.size();
size_t charsToAbsorb = longestMatch;
// If the resolved alias ends in a path separator, do not consume it.
const bool resolvedAliasEndsInPathSeparator = (longestResolvedAlias.ends_with(AZ::IO::PosixPathSeparator) ||
longestResolvedAlias.ends_with(AZ::IO::WindowsPathSeparator));
const size_t charsToAbsorb = resolvedAliasEndsInPathSeparator ? longestMatch - 1 : longestMatch;
size_t remainingData = bufStringLength - charsToAbsorb;
size_t finalStringSize = aliasSize + remainingData;
if (finalStringSize >= outBufferLength)
@@ -27,28 +27,4 @@ namespace AzFramework
;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputChannelId::GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::Crc32& InputChannelId::GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputChannelId::operator==(const InputChannelId& other) const
{
return (m_crc32 == other.m_crc32);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputChannelId::operator!=(const InputChannelId& other) const
{
return !(*this == other);
}
} // namespace AzFramework
@@ -39,53 +39,58 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Constructor
//! \param[in] name Name of the input channel (will be ignored if exceeds MAX_NAME_LENGTH)
//! \param[in] name Name of the input channel (will be truncated if exceeds MAX_NAME_LENGTH)
explicit constexpr InputChannelId(AZStd::string_view name = "")
: m_name(name)
, m_crc32(name)
: m_name(name.substr(0, MAX_NAME_LENGTH))
, m_crc32(name.substr(0, MAX_NAME_LENGTH))
{
}
constexpr InputChannelId(const InputChannelId& other) = default;
constexpr InputChannelId(InputChannelId&& other) = default;
constexpr InputChannelId& operator=(const InputChannelId& other)
{
m_name = other.m_name;
m_crc32 = other.m_crc32;
return *this;
}
constexpr InputChannelId& operator=(InputChannelId&& other)
{
m_name = AZStd::move(other.m_name);
m_crc32 = AZStd::move(other.m_crc32);
other.m_crc32 = 0;
return *this;
}
////////////////////////////////////////////////////////////////////////////////////////////
// Default copying and moving
AZ_DEFAULT_COPY_MOVE(InputChannelId);
////////////////////////////////////////////////////////////////////////////////////////////
//! Default destructor
~InputChannelId() = default;
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input channel's name
//! \return Name of the input channel
const char* GetName() const;
constexpr const char* GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the crc32 of the input channel's name
//! \return crc32 of the input channel name
const AZ::Crc32& GetNameCrc32() const;
constexpr const AZ::Crc32& GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
///@{
//! Equality comparison operator
//! \param[in] other Another instance of the class to compare for equality
bool operator==(const InputChannelId& other) const;
bool operator!=(const InputChannelId& other) const;
///@}
constexpr bool operator==(const InputChannelId& other) const
{
return m_crc32 == other.m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Inequality comparison operator
//! \param[in] other Another instance of the class to compare for inequality
constexpr bool operator!=(const InputChannelId& other) const
{
return !(*this == other);
}
private:
////////////////////////////////////////////////////////////////////////////////////////////
// Variables
AZStd::fixed_string<MAX_NAME_LENGTH> m_name; //!< Name of the input channel
AZ::Crc32 m_crc32; //!< Crc32 of the input channel
AZ::Crc32 m_crc32; //!< Crc32 of the input channel name
};
} // namespace AzFramework
@@ -14,14 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputDeviceGamepad::Name("gamepad");
const InputDeviceId InputDeviceGamepad::IdForIndex0(Name, 0);
const InputDeviceId InputDeviceGamepad::IdForIndex1(Name, 1);
const InputDeviceId InputDeviceGamepad::IdForIndex2(Name, 2);
const InputDeviceId InputDeviceGamepad::IdForIndex3(Name, 3);
const InputDeviceId InputDeviceGamepad::IdForIndexN(AZ::u32 n) { return InputDeviceId(Name, n); }
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceGamepad::IsGamepadDevice(const InputDeviceId& inputDeviceId)
{
@@ -32,16 +32,16 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The name used to identify any game-pad input device
static const char* Name;
static constexpr inline const char* Name{"gamepad"};
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify a game-pad input device with a specific index
///@{
static const InputDeviceId IdForIndex0;
static const InputDeviceId IdForIndex1;
static const InputDeviceId IdForIndex2;
static const InputDeviceId IdForIndex3;
static const InputDeviceId IdForIndexN(AZ::u32 n);
static constexpr inline InputDeviceId IdForIndex0{Name, 0};
static constexpr inline InputDeviceId IdForIndex1{Name, 1};
static constexpr inline InputDeviceId IdForIndex2{Name, 2};
static constexpr inline InputDeviceId IdForIndex3{Name, 3};
static constexpr inline InputDeviceId IdForIndexN(AZ::u32 n) { return InputDeviceId(Name, n); }
///@}
////////////////////////////////////////////////////////////////////////////////////////////
@@ -29,71 +29,4 @@ namespace AzFramework
;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId::InputDeviceId(const char* name, AZ::u32 index)
: m_crc32(name)
, m_index(index)
{
memset(m_name, 0, AZ_ARRAY_SIZE(m_name));
azstrncpy(m_name, NAME_BUFFER_SIZE, name, MAX_NAME_LENGTH);
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId::InputDeviceId(const InputDeviceId& other)
: m_crc32(other.m_crc32)
, m_index(other.m_index)
{
memset(m_name, 0, AZ_ARRAY_SIZE(m_name));
azstrcpy(m_name, NAME_BUFFER_SIZE, other.m_name);
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId& InputDeviceId::operator=(const InputDeviceId& other)
{
azstrcpy(m_name, NAME_BUFFER_SIZE, other.m_name);
m_crc32 = other.m_crc32;
m_index = other.m_index;
return *this;
}
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputDeviceId::GetName() const
{
return m_name;
}
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::Crc32& InputDeviceId::GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////////
AZ::u32 InputDeviceId::GetIndex() const
{
return m_index;
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator==(const InputDeviceId& other) const
{
return (m_crc32 == other.m_crc32) && (m_index == other.m_index);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator!=(const InputDeviceId& other) const
{
return !(*this == other);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator<(const InputDeviceId& other) const
{
if (m_index == other.m_index)
{
return m_crc32 < other.m_crc32;
}
return m_index < other.m_index;
}
} // namespace AzFramework
@@ -11,6 +11,7 @@
#include <AzCore/Math/Crc.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/std/hash.h>
#include <AzCore/std/string/fixed_string.h>
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
@@ -22,8 +23,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
// Constants
static const int NAME_BUFFER_SIZE = 64;
static const int MAX_NAME_LENGTH = NAME_BUFFER_SIZE - 1;
static constexpr int MAX_NAME_LENGTH = 64;
////////////////////////////////////////////////////////////////////////////////////////////
// Allocator
@@ -41,17 +41,16 @@ namespace AzFramework
//! Constructor
//! \param[in] name Name of the input device (will be truncated if exceeds MAX_NAME_LENGTH)
//! \param[in] index Index of the input device (optional)
explicit InputDeviceId(const char* name, AZ::u32 index = 0);
explicit constexpr InputDeviceId(AZStd::string_view name, AZ::u32 index = 0)
: m_name(name.substr(0, MAX_NAME_LENGTH))
, m_crc32(name.substr(0, MAX_NAME_LENGTH))
, m_index(index)
{
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Copy constructor
//! \param[in] other Another instance of the class to copy from
InputDeviceId(const InputDeviceId& other);
////////////////////////////////////////////////////////////////////////////////////////////
//! Copy assignment operator
//! \param[in] other Another instance of the class to copy from
InputDeviceId& operator=(const InputDeviceId& other);
// Default copying and moving
AZ_DEFAULT_COPY_MOVE(InputDeviceId);
////////////////////////////////////////////////////////////////////////////////////////////
//! Default destructor
@@ -60,12 +59,18 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input device's name
//! \return Name of the input device
const char* GetName() const;
constexpr const char* GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the crc32 of the input device's name
//! \return crc32 of the input device name
const AZ::Crc32& GetNameCrc32() const;
constexpr const AZ::Crc32& GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input device's index. Used for differentiating between multiple instances
@@ -75,27 +80,45 @@ namespace AzFramework
//! at startup using indicies 0->3. As gamepads connect/disconnect at runtime we assign the
//! appropriate (system dependent) local user id (see InputDevice::GetAssignedLocalUserId).
//! \return Index of the input device
AZ::u32 GetIndex() const;
constexpr AZ::u32 GetIndex() const
{
return m_index;
}
////////////////////////////////////////////////////////////////////////////////////////////
///@{
//! Equality comparison operator
//! \param[in] other Another instance of the class to compare for equality
bool operator==(const InputDeviceId& other) const;
bool operator!=(const InputDeviceId& other) const;
///@}
constexpr bool operator==(const InputDeviceId& other) const
{
return (m_crc32 == other.m_crc32) && (m_index == other.m_index);
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Inequality comparison operator
//! \param[in] other Another instance of the class to compare for inequality
constexpr bool operator!=(const InputDeviceId& other) const
{
return !(*this == other);
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Less than comparison operator
//! \param[in] other Another instance of the class to compare
bool operator<(const InputDeviceId& other) const;
constexpr bool operator<(const InputDeviceId& other) const
{
if (m_index == other.m_index)
{
return m_crc32 < other.m_crc32;
}
return m_index < other.m_index;
}
private:
////////////////////////////////////////////////////////////////////////////////////////////
// Variables
char m_name[NAME_BUFFER_SIZE]; //!< Name of the input device
AZ::Crc32 m_crc32; //!< Crc32 of the input device
AZ::u32 m_index; //!< Index of the input device
AZStd::fixed_string<MAX_NAME_LENGTH> m_name; //!< Name of the input device
AZ::Crc32 m_crc32; //!< Crc32 of the input device name
AZ::u32 m_index; //!< Index of the input device
};
} // namespace AzFramework
@@ -15,9 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceKeyboard::Id("keyboard");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceKeyboard::IsKeyboardDevice(const InputDeviceId& inputDeviceId)
{
@@ -33,7 +33,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary physical keyboard input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"keyboard"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a physical keyboard (regardless of index)
@@ -14,9 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceMotion::Id("motion");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceMotion::IsMotionDevice(const InputDeviceId& inputDeviceId)
{
@@ -28,7 +28,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary motion input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"motion"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a motion device (regardless of index)
@@ -15,18 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateDefault = 60;
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateQueueAll = std::numeric_limits<AZ::u32>::max();
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateAccumulateAll = 0;
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceMouse::Id("mouse");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceMouse::IsMouseDevice(const InputDeviceId& inputDeviceId)
{
@@ -31,23 +31,23 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Default sample rate for raw mouse movement events that aims to strike a balance between
//! responsiveness and performance.
static const AZ::u32 MovementSampleRateDefault;
static constexpr inline AZ::u32 MovementSampleRateDefault{60};
////////////////////////////////////////////////////////////////////////////////////////////
//! Sample rate for raw mouse movement that will cause all events received in the same frame
//! to be queued and dispatched as individual events. This results in maximum responsiveness
//! but may potentially impact performance depending how many events happen over each frame.
static const AZ::u32 MovementSampleRateQueueAll;
static constexpr inline AZ::u32 MovementSampleRateQueueAll{std::numeric_limits<AZ::u32>::max()};
////////////////////////////////////////////////////////////////////////////////////////////
//! Sample rate for raw mouse movement that will cause all events received in the same frame
//! to be accumulated and dispatched as a single event. Optimal for performance, but results
//! in sluggish/unresponsive mouse movement, especially when running at low frame rates.
static const AZ::u32 MovementSampleRateAccumulateAll;
static constexpr inline AZ::u32 MovementSampleRateAccumulateAll{0};
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary mouse input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"mouse"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a mouse (regardless of index)
@@ -15,9 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceTouch::Id("touch");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceTouch::IsTouchDevice(const InputDeviceId& inputDeviceId)
{
@@ -25,7 +25,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary touch input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"touch"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a touch device (regardless of index)
@@ -14,9 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceVirtualKeyboard::Id("virtual_keyboard");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceVirtualKeyboard::IsVirtualKeyboardDevice(const InputDeviceId& inputDeviceId)
{
@@ -25,7 +25,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary virtual keyboard input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"virtual_keyboard"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a virtual keyboard (regardless of index)
@@ -30,12 +30,22 @@ namespace AzFramework
//! Called when the root spawnable has been assigned a new value. This may be called several times without a call to release
//! in between.
//! @note: The callback is not queued but immediately called from a random thread. This is done because this callback is typically
//! used before entities are spawned and if it's queued then the entities spawn before this callback is called.
//! @param rootSpawnable The new root spawnable that was assigned.
//! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned.
virtual void OnRootSpawnableAssigned([[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable,
[[maybe_unused]] uint32_t generation) {}
//! Called when the root spawnable has completed spawning of entities. This may be called several times without a call to release
//! in between.
//! @note: This callback is queued and will be called with a delay and from the main thread.
//! @param rootSpawnable The new root spawnable that was used to spawn entities from.
//! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned.
virtual void OnRootSpawnableReady(
[[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable, [[maybe_unused]] uint32_t generation) {}
//! Called when the root spawnable has Released. This will only be called if there's no root spawnable assigned to take the
//! place of the original root spawnable.
//! Note: This callback is queued and will be called with a delay and from the main thread.
//! @param generation The generation of the root spawnable that was released.
virtual void OnRootSpawnableReleased([[maybe_unused]] uint32_t generation) {}
};
@@ -6,12 +6,473 @@
*
*/
#include <AzCore/Asset/AssetSerializer.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/numeric.h>
#include <AzCore/std/sort.h>
#include <AzCore/std/typetraits/typetraits.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AzFramework
{
//
// EntityAlias
//
bool Spawnable::EntityAlias::HasLowerIndex(const EntityAlias& other) const
{
return m_sourceIndex == other.m_sourceIndex ?
m_aliasType < other.m_aliasType :
m_sourceIndex < other.m_sourceIndex;
}
//
// EntityAliasVisitorBase
//
bool Spawnable::EntityAliasVisitorBase::IsValid(const EntityAliasList* aliases) const
{
return aliases != nullptr;
}
bool Spawnable::EntityAliasVisitorBase::HasAliases(const EntityAliasList* aliases) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return !aliases->empty();
}
bool Spawnable::EntityAliasVisitorBase::AreAllSpawnablesReady(const EntityAliasList* aliases) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (const EntityAlias& alias : *aliases)
{
if (!alias.m_queueLoad ||
alias.m_aliasType == Spawnable::EntityAliasType::Original ||
alias.m_aliasType == Spawnable::EntityAliasType::Disable)
{
continue;
}
if (!alias.m_spawnable.IsReady() && !alias.m_spawnable.IsError())
{
return false;
}
}
return true;
}
auto Spawnable::EntityAliasVisitorBase::begin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cbegin();
}
auto Spawnable::EntityAliasVisitorBase::end(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cend();
}
auto Spawnable::EntityAliasVisitorBase::cbegin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cbegin();
}
auto Spawnable::EntityAliasVisitorBase::cend(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cend();
}
void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables(
const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZStd::unordered_set<AZ::Data::AssetId> spawnableIds;
for (const Spawnable::EntityAlias& alias : *aliases)
{
// If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on.
if (alias.m_spawnable.GetId().IsValid())
{
auto it = spawnableIds.find(alias.m_spawnable.GetId());
if (it == spawnableIds.end())
{
callback(alias.m_spawnable);
spawnableIds.emplace(alias.m_spawnable.GetId());
}
}
}
}
void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables(
const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZStd::unordered_set<AZ::Data::AssetId> spawnableIds;
for (const Spawnable::EntityAlias& alias : *aliases)
{
// If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on.
if (alias.m_tag == tag && alias.m_spawnable.GetId().IsValid())
{
auto it = spawnableIds.find(alias.m_spawnable.GetId());
if (it == spawnableIds.end())
{
callback(alias.m_spawnable);
spawnableIds.emplace(alias.m_spawnable.GetId());
}
}
}
}
//
// EntityAliasVisitor
//
Spawnable::EntityAliasVisitor::EntityAliasVisitor(Spawnable& owner, EntityAliasList* entityAliasList)
: m_owner(owner)
, m_entityAliasList(entityAliasList)
{
}
Spawnable::EntityAliasVisitor::~EntityAliasVisitor()
{
if (IsValid())
{
Optimize();
AZ_Assert(
m_owner.m_shareState == ShareState::ReadWrite, "Attempting to unlock a spawnable that's not in the locked state (%i).",
m_owner.m_shareState.load());
m_owner.m_shareState = ShareState::NotShared;
}
}
Spawnable::EntityAliasVisitor::EntityAliasVisitor(EntityAliasVisitor&& rhs)
: m_owner(rhs.m_owner)
, m_entityAliasList(rhs.m_entityAliasList)
{
m_dirty = rhs.m_dirty;
rhs.m_entityAliasList = nullptr;
rhs.m_dirty = false;
}
auto Spawnable::EntityAliasVisitor::operator=(EntityAliasVisitor&& rhs) -> EntityAliasVisitor&
{
if (this != &rhs)
{
this->~EntityAliasVisitor();
new(this) EntityAliasVisitor(AZStd::move(rhs));
}
return *this;
}
bool Spawnable::EntityAliasVisitor::IsValid() const
{
return EntityAliasVisitorBase::IsValid(m_entityAliasList);
}
bool Spawnable::EntityAliasVisitor::HasAliases() const
{
return EntityAliasVisitorBase::HasAliases(m_entityAliasList);
}
bool Spawnable::EntityAliasVisitor::AreAllSpawnablesReady() const
{
return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::begin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::begin(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::end() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::end(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::cbegin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cbegin(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::cend() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cend(m_entityAliasList);
}
void Spawnable::EntityAliasVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback);
}
void Spawnable::EntityAliasVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback);
}
void Spawnable::EntityAliasVisitor::AddAlias(
AZ::Data::Asset<Spawnable> targetSpawnable,
AZ::Crc32 tag,
uint32_t sourceIndex,
uint32_t targetIndex,
Spawnable::EntityAliasType aliasType,
bool queueLoad)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZ_Assert(sourceIndex < m_owner.GetEntities().size(), "Invalid source index (%i) for entity alias", sourceIndex);
if (targetSpawnable.IsReady())
{
AZ_Assert(
targetIndex < targetSpawnable->GetEntities().size(), "Invalid target index (%i) for entity alias '%s'", targetIndex,
targetSpawnable.GetHint().c_str());
}
m_entityAliasList->push_back(Spawnable::EntityAlias{ targetSpawnable, tag, sourceIndex, targetIndex, aliasType, queueLoad });
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
if (alias.m_queueLoad &&
alias.m_aliasType != Spawnable::EntityAliasType::Original &&
alias.m_aliasType != Spawnable::EntityAliasType::Disable &&
!alias.m_spawnable.IsLoading() &&
!alias.m_spawnable.IsReady() &&
!alias.m_spawnable.IsError())
{
callback(alias.m_spawnable);
}
}
}
void Spawnable::EntityAliasVisitor::UpdateAliases(const UpdateCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
AZ::Data::Asset<Spawnable> targetSpawnable(alias.m_spawnable);
callback(alias.m_aliasType, alias.m_queueLoad, targetSpawnable, alias.m_tag, alias.m_sourceIndex, alias.m_targetIndex);
}
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::UpdateAliases(AZ::Crc32 tag, const UpdateCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
if (alias.m_tag == tag)
{
AZ::Data::Asset<Spawnable> targetSpawnable(alias.m_spawnable);
callback(alias.m_aliasType, alias.m_queueLoad, targetSpawnable, alias.m_tag, alias.m_sourceIndex, alias.m_targetIndex);
m_dirty = true;
}
}
}
void Spawnable::EntityAliasVisitor::UpdateAliasType(uint32_t index, Spawnable::EntityAliasType newType)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZ_Assert(
index < m_entityAliasList->size(), "Unable to update entity alias at index %i as there are only %zu aliases in spawnable.",
index, m_entityAliasList->size());
(*m_entityAliasList)[index].m_aliasType = newType;
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::Optimize()
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
if (m_dirty)
{
AZStd::stable_sort(
m_entityAliasList->begin(), m_entityAliasList->end(),
[](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs)
{
// Sort by source index from smallest to largest so during spawning the entities can be iterated linearly over.
// If the source index is the same then sort by alias type so the next steps can optimize away superfluous steps.
return lhs.HasLowerIndex(rhs);
});
// Remove aliases that are not going to have any practical effect and insert aliases where needed to simplify the spawning.
// This is done at runtime rather than at build time because the above ebus allows other systems to make adjustments to the
// aliases, for instance Networking can decide to disable certain aliases when running on a client. This in turn also requires
// the aliases to be in their recorded order during building as the ebus handlers may depend on that order to determine what
// entities need to be updated.
uint32_t previousIndex = AZStd::numeric_limits<uint32_t>::max();
Spawnable::EntityAliasType previousType =
static_cast<Spawnable::EntityAliasType>(AZStd::numeric_limits<AZStd::underlying_type_t<Spawnable::EntityAliasType>>::max());
Spawnable::EntityAlias* it = m_entityAliasList->begin();
Spawnable::EntityAlias* end = m_entityAliasList->end();
while (it < end)
{
// If there's a switch to a new source index and the previous index only had an original it can
// be removed.
if (previousType == Spawnable::EntityAliasType::Original && previousIndex != it->m_sourceIndex)
{
it = m_entityAliasList->erase(it - 1);
end = m_entityAliasList->end();
if (it == end)
{
break;
}
}
switch (it->m_aliasType)
{
case Spawnable::EntityAliasType::Original:
[[fallthrough]];
case Spawnable::EntityAliasType::Disable:
[[fallthrough]];
case Spawnable::EntityAliasType::Replace:
// If the previous entry was a disabled, original or replace alias then remove it as it will be overwritten by the
// current entry.
if (previousIndex == it->m_sourceIndex &&
(previousType == Spawnable::EntityAliasType::Original ||
previousType == Spawnable::EntityAliasType::Disable ||
previousType == Spawnable::EntityAliasType::Replace))
{
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
// Erase instead of a swap-and-pop in order to preserver the order.
it = m_entityAliasList->erase(it - 1) + 1;
end = m_entityAliasList->end();
}
else
{
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
++it;
}
break;
case Spawnable::EntityAliasType::Additional:
[[fallthrough]];
case Spawnable::EntityAliasType::Merge:
// If this is the first entry for this index then insert an original in front of it so the spawnable entity manager
// doesn't have to check for the case there's a merge and/or addition without an entity to extend.
if (previousIndex != it->m_sourceIndex)
{
Spawnable::EntityAlias insert;
// No load, as the asset is already loaded.
insert.m_spawnable = AZ::Data::Asset<Spawnable>({}, azrtti_typeid<Spawnable>());
insert.m_sourceIndex = it->m_sourceIndex;
insert.m_targetIndex = it->m_sourceIndex; // Source index as the original entry for this slot is added.
insert.m_aliasType = Spawnable::EntityAliasType::Original;
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
// Insert to maintain the order.
it = m_entityAliasList->insert(it, AZStd::move(insert));
it += 2;
end = m_entityAliasList->end();
}
else
{
previousType = it->m_aliasType;
++it;
}
break;
default:
AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", it->m_aliasType);
break;
}
}
// Check if the last entry is an "Original" in which case it can be removed.
if (!m_entityAliasList->empty() && m_entityAliasList->back().m_aliasType == Spawnable::EntityAliasType::Original)
{
m_entityAliasList->pop_back();
}
// Reclaim memory because after this point the aliases will not change anymore.
m_entityAliasList->shrink_to_fit();
m_dirty = false;
}
}
//
// EntityAliasConstVisitor
//
Spawnable::EntityAliasConstVisitor::EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList)
: m_owner(owner)
, m_entityAliasList(entityAliasList)
{
}
Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor()
{
if (IsValid())
{
AZ_Assert(
m_owner.m_shareState <= ShareState::Read, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).",
m_owner.m_shareState.load());
m_owner.m_shareState++;
}
}
bool Spawnable::EntityAliasConstVisitor::IsValid() const
{
return EntityAliasVisitorBase::IsValid(m_entityAliasList);
}
bool Spawnable::EntityAliasConstVisitor::HasAliases() const
{
return EntityAliasVisitorBase::HasAliases(m_entityAliasList);
}
bool Spawnable::EntityAliasConstVisitor::AreAllSpawnablesReady() const
{
return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::begin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::begin(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::end() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::end(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::cbegin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cbegin(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::cend() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cend(m_entityAliasList);
}
void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback);
}
void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback);
}
//
// Spawnable
//
Spawnable::Spawnable(const AZ::Data::AssetId& id, AssetStatus status)
: AZ::Data::AssetData(id, status)
{
@@ -27,6 +488,33 @@ namespace AzFramework
return m_entities;
}
auto Spawnable::TryGetAliasesConst() const -> EntityAliasConstVisitor
{
int32_t expected = ShareState::NotShared;
do
{
// Try to set the lock to a negative number to indicate a shared read.
if (m_shareState.compare_exchange_strong(expected, expected - 1))
{
return EntityAliasConstVisitor(*this, &m_entityAliases);
}
// as long as the value is negative or not shared then keep trying to get a shared read lock.
} while (expected <= 0);
return EntityAliasConstVisitor(*this, nullptr);
}
auto Spawnable::TryGetAliases() const -> EntityAliasConstVisitor
{
return TryGetAliasesConst();
}
auto Spawnable::TryGetAliases() -> EntityAliasVisitor
{
int32_t expected = ShareState::NotShared;
return m_shareState.compare_exchange_strong(expected, ShareState::ReadWrite) ? EntityAliasVisitor(*this, &m_entityAliases)
: EntityAliasVisitor(*this, nullptr);
}
bool Spawnable::IsEmpty() const
{
return m_entities.empty();
@@ -44,11 +532,29 @@ namespace AzFramework
void Spawnable::Reflect(AZ::ReflectContext* context)
{
EntityAlias::Reflect(context);
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(1)
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(2)
->Field("Meta data", &Spawnable::m_metaData)
->Field("Entity aliases", &Spawnable::m_entityAliases)
->Field("Entities", &Spawnable::m_entities);
}
}
void Spawnable::EntityAlias::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<Spawnable::EntityAlias>()
->Version(1)
->Field("Spawnable", &EntityAlias::m_spawnable)
->Field("Tag", &EntityAlias::m_tag)
->Field("Source Index", &EntityAlias::m_sourceIndex)
->Field("Target Index", &EntityAlias::m_targetIndex)
->Field("Alias Type", &EntityAlias::m_aliasType)
->Field("Queue Load", &EntityAlias::m_queueLoad);
}
}
} // namespace AzFramework
@@ -11,6 +11,7 @@
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/parallel/atomic.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Spawnable/SpawnableMetaData.h>
@@ -29,7 +30,148 @@ namespace AzFramework
AZ_CLASS_ALLOCATOR(Spawnable, AZ::SystemAllocator, 0);
AZ_RTTI(AzFramework::Spawnable, "{855E3021-D305-4845-B284-20C3F7FDF16B}", AZ::Data::AssetData);
// The order is important for sorting in the SpawnableAssetHandler.
enum class EntityAliasType : uint8_t
{
Original, //!< The original entity is spawned.
Disable, //!< No entity will be spawned.
Replace, //!< The entity alias is spawned instead of the original.
Additional, //!< The original entity is spawned as well as the alias. The alias will get a new entity id.
Merge //!< The original entity is spawned and the components of the alias are added. The caller is responsible for
//!< maintaining a valid component list.
};
enum ShareState : int32_t
{
Read = -1,
NotShared = 0,
ReadWrite = 1
};
//! An entity alias redirects the spawning of an entity to another entity, possibly in another spawnable.
struct EntityAlias
{
AZ_CLASS_ALLOCATOR(EntityAlias, AZ::SystemAllocator, 0);
AZ_TYPE_INFO(AzFramework::Spawnable::EntityAlias, "{C8D0C5BC-1F0B-4572-98C1-73B2CA8C9356}");
bool HasLowerIndex(const EntityAlias& other) const;
AZ::Data::Asset<Spawnable> m_spawnable; //!< The spawnable containing the target entity to spawn.
uint32_t m_tag{ 0 }; //!< A unique tag to identify this alias with.
uint32_t m_sourceIndex{ 0 }; //!< The index of the entity in the original spawnable that will be replaced.
uint32_t m_targetIndex{ 0 }; //!< The index of the entity in the target spawnable that will be used to replace the original.
EntityAliasType m_aliasType{ EntityAliasType::Original }; //!< The kind of replacement.
bool m_queueLoad{ false }; //!< Whether or not to automatically queue the spawnable for loading.
static void Reflect(AZ::ReflectContext* context);
};
using EntityList = AZStd::vector<AZStd::unique_ptr<AZ::Entity>>;
using EntityAliasList = AZStd::vector<EntityAlias>;
private:
class EntityAliasVisitorBase
{
protected:
bool IsValid(const EntityAliasList* aliases) const;
bool HasAliases(const EntityAliasList* aliases) const;
bool AreAllSpawnablesReady(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator begin(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator end(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator cbegin(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator cend(const EntityAliasList* aliases) const;
using ListTargetSpawanblesCallback = AZStd::function<void(const AZ::Data::Asset<Spawnable>& targetSpawnable)>;
void ListTargetSpawnables(const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
};
public:
class EntityAliasVisitor final : public EntityAliasVisitorBase
{
public:
EntityAliasVisitor(Spawnable& owner, EntityAliasList* m_entityAliasList);
~EntityAliasVisitor();
EntityAliasVisitor(EntityAliasVisitor&& rhs);
EntityAliasVisitor& operator=(EntityAliasVisitor&& rhs);
EntityAliasVisitor(const EntityAliasVisitor& rhs) = delete;
EntityAliasVisitor& operator=(const EntityAliasVisitor& rhs) = delete;
//! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions.
bool IsValid() const;
bool HasAliases() const;
bool AreAllSpawnablesReady() const;
// Modification of aliases is limited to specific changes that can only be done through the available modification functions.
// For this reason access through iterators is limited to unmodifiable constant iterators.
EntityAliasList::const_iterator begin() const;
EntityAliasList::const_iterator end() const;
EntityAliasList::const_iterator cbegin() const;
EntityAliasList::const_iterator cend() const;
void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
void AddAlias(
AZ::Data::Asset<Spawnable> targetSpawnable,
AZ::Crc32 tag,
uint32_t sourceIndex,
uint32_t targetIndex,
Spawnable::EntityAliasType aliasType,
bool queueLoad);
using ListSpawnablesRequiringLoadCallback = AZStd::function<void(AZ::Data::Asset<Spawnable>& spawnablePendingLoad)>;
void ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback);
using UpdateCallback = AZStd::function<void(
Spawnable::EntityAliasType& aliasType,
bool& queueLoad,
const AZ::Data::Asset<Spawnable>& aliasedSpawnable,
const AZ::Crc32 tag,
const uint32_t sourceIndex,
const uint32_t targetIndex)>;
void UpdateAliases(const UpdateCallback& callback);
void UpdateAliases(AZ::Crc32 tag, const UpdateCallback& callback);
void UpdateAliasType(uint32_t index, Spawnable::EntityAliasType newType);
void Optimize();
private:
Spawnable& m_owner;
EntityAliasList* m_entityAliasList{ nullptr };
bool m_dirty{ false };
};
class EntityAliasConstVisitor final : public EntityAliasVisitorBase
{
public:
EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList);
~EntityAliasConstVisitor();
//! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions.
bool IsValid() const;
bool HasAliases() const;
bool AreAllSpawnablesReady() const;
EntityAliasList::const_iterator begin() const;
EntityAliasList::const_iterator end() const;
EntityAliasList::const_iterator cbegin() const;
EntityAliasList::const_iterator cend() const;
void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
private:
const Spawnable& m_owner;
const EntityAliasList* m_entityAliasList;
};
inline static constexpr const char* FileExtension = "spawnable";
inline static constexpr const char* DotFileExtension = ".spawnable";
@@ -45,6 +187,9 @@ namespace AzFramework
const EntityList& GetEntities() const;
EntityList& GetEntities();
EntityAliasConstVisitor TryGetAliasesConst() const;
EntityAliasConstVisitor TryGetAliases() const;
EntityAliasVisitor TryGetAliases();
bool IsEmpty() const;
SpawnableMetaData& GetMetaData();
@@ -55,11 +200,12 @@ namespace AzFramework
private:
SpawnableMetaData m_metaData;
// Aliases that optionally replace the ones stored in this spawnable.
EntityAliasList m_entityAliases;
// Container for keeping all entities of the prefab the Spawnable was created from.
// Includes both direct and nested entities of the prefab.
EntityList m_entities;
mutable AZStd::atomic<int32_t> m_shareState{ ShareState::NotShared };
};
using SpawnableList = AZStd::vector<Spawnable>;
} // namespace AzFramework
@@ -0,0 +1,38 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/std/parallel/mutex.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AzFramework
{
class SpawnableAssetEvents : public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
//! Callback to allow the entity aliases in a spawnable to adjusted based on runtime requirements.
//! This will be called by the Asset Manager as part of the creation of the spawnable asset from loaded file data. Any work done
//! in this callback will be counted towards the maximum amount of time allocated to asset handlers to construct their assets,
//! it's recommended to keep work done in this callback to a minimum and prefer delaying any complex processing.
//!
//! ALERT: Do not start blocking asset requests in this callback.
//! Since this is part of the Asset Manager's asset streaming, doing a blocking load in this callback will cause the job
//! processing the spawnable asset to locked out of doing any asset streaming work. If there are more spawnables doing
//! this than there are job threads available the engine will enter a deadlock situation as no more assets can complete
//! loading and no job threads become free as they're all waiting for assets to complete. It is however safe to queue
//! an asset for loading.
virtual void OnResolveAliases(
Spawnable::EntityAliasVisitor& aliases, const SpawnableMetaData& metadata, const Spawnable::EntityList& entities) = 0;
};
using SpawnableAssetEventsBus = AZ::EBus<SpawnableAssetEvents>;
} // namespace AzFramework
@@ -9,8 +9,10 @@
#include <AzCore/Casting/lossy_cast.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/sort.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
#include <AzFramework/Spawnable/SpawnableAssetBus.h>
namespace AzFramework
{
@@ -52,6 +54,7 @@ namespace AzFramework
AZ::ObjectStream::FilterDescriptor filter(assetLoadFilterCB);
if (AZ::Utils::LoadObjectFromStreamInPlace(*stream, *spawnable, nullptr /*SerializeContext*/, filter))
{
ResolveEntityAliases(spawnable, asset, stream->GetStreamingDeadline(), stream->GetStreamingPriority(), assetLoadFilterCB);
return AZ::Data::AssetHandler::LoadResult::LoadComplete;
}
else
@@ -91,4 +94,41 @@ namespace AzFramework
AZ::Uuid subIdHash = AZ::Uuid::CreateData(id.data(), id.size());
return azlossy_caster(subIdHash.GetHash());
}
void SpawnableAssetHandler::ResolveEntityAliases(
Spawnable* spawnable,
[[maybe_unused]] const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::chrono::milliseconds streamingDeadline,
AZ::IO::IStreamerTypes::Priority streamingPriority,
const AZ::Data::AssetFilterCB& assetLoadFilterCB)
{
Spawnable::EntityAliasVisitor aliases = spawnable->TryGetAliases();
AZ_Assert(aliases.IsValid(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str());
if (aliases.HasAliases())
{
AZ_Assert(
AZStd::is_sorted(
aliases.begin(), aliases.end(),
[](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs)
{
return lhs.HasLowerIndex(rhs);
}),
"Spawnable '%s' has an unsorted entity alias list.", asset.GetHint().c_str());
SpawnableAssetEventsBus::Broadcast(
&SpawnableAssetEvents::OnResolveAliases, aliases, spawnable->GetMetaData(), spawnable->GetEntities());
// The aliases will only be optimized if OnResolveAliases has made any changes.
aliases.Optimize();
aliases.ListSpawnablesRequiringLoad(
[&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset<Spawnable>& assetPendingLoad)
{
AZ::Data::AssetLoadParameters loadInfo;
loadInfo.m_assetLoadFilterCB = assetLoadFilterCB;
loadInfo.m_deadline = streamingDeadline;
loadInfo.m_priority = streamingPriority;
assetPendingLoad.QueueLoad(loadInfo);
});
}
}
} // namespace AzFramework
@@ -50,5 +50,13 @@ namespace AzFramework
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
const AZ::Data::AssetFilterCB& assetLoadFilterCB) override;
private:
void ResolveEntityAliases(
class Spawnable* spawnable,
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::chrono::milliseconds streamingDeadline,
AZ::IO::IStreamerTypes::Priority streamingPriority,
const AZ::Data::AssetFilterCB& assetLoadFilterCB);
};
} // namespace AzFramework
@@ -26,7 +26,7 @@ namespace AzFramework
return m_threadData != nullptr;
}
uint64_t SpawnableEntitiesContainer::GetCurrentGeneration() const
uint32_t SpawnableEntitiesContainer::GetCurrentGeneration() const
{
return m_currentGeneration;
}
@@ -37,7 +37,7 @@ namespace AzFramework
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
}
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<size_t> entityIndices)
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<uint32_t> entityIndices)
{
AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnEntities(
@@ -78,15 +78,21 @@ namespace AzFramework
}
}
void SpawnableEntitiesContainer::Alert(AlertCallback callback)
void SpawnableEntitiesContainer::Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck)
{
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
[generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
});
auto callbackWrapper = [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
};
if (spawnableCheck == CheckIfSpawnableIsLoaded::No)
{
SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket, AZStd::move(callbackWrapper));
}
else
{
SpawnableEntitiesInterface::Get()->LoadBarrier(m_threadData->m_spawnedEntitiesTicket, AZStd::move(callbackWrapper));
}
}
void SpawnableEntitiesContainer::Connect(AZ::Data::Asset<Spawnable> spawnable)
@@ -36,6 +36,12 @@ namespace AzFramework
public:
using AlertCallback = AZStd::function<void(uint32_t generation)>;
enum class CheckIfSpawnableIsLoaded : bool
{
Yes,
No
};
//! Constructs a new spawnables entity container that has not been connected.
SpawnableEntitiesContainer() = default;
//! Constructs a new spawnables entity container that connects to the provided spawnable.
@@ -48,13 +54,13 @@ namespace AzFramework
//! Returns a number that identifies the current generation of the container with. The completion callback can still receive
//! calls from older generations as processing completes on those. The returned value can be used to help calls tell
//! older versions apart from newer ones.
[[nodiscard]] uint64_t GetCurrentGeneration() const;
[[nodiscard]] uint32_t GetCurrentGeneration() const;
//! Puts in a request to spawn entities using all entities in the provided spawnable as a template.
void SpawnAllEntities();
//! Puts in a request to spawn entities using the entities found in the spawnable at the provided indices as a template.
//! @param entityIndices A list of indices to the entities in the spawnable.
void SpawnEntities(AZStd::vector<size_t> entityIndices);
void SpawnEntities(AZStd::vector<uint32_t> entityIndices);
//! Puts in a request to despawn all previous spawned entities.
void DespawnAllEntities();
@@ -73,7 +79,11 @@ namespace AzFramework
//! other than the calling thread including the main thread. Note that because the alert is queued it can still be called
//! after the container has been deleted or can be called for a previously assigned spawnable. In the latter case check
//! if the current generation matches the generation provided with the callback.
void Alert(AlertCallback callback);
//! @param callback The function called when the alert triggers. This can be called from a different thread than the one that
//! the one that made the call to Alert.
//! @param checkSpawnableIsLoaded If true the alert will also block until the spawnable has been loaded. If false then it will
//! be called after all previous calls have completed, but the spawnable may not be loaded at that point.
void Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck = CheckIfSpawnableIsLoaded::No);
private:
void Connect(AZ::Data::Asset<Spawnable> spawnable);
@@ -152,7 +152,7 @@ namespace AzFramework
// SpawnableIndexEntityPair
//
SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator)
SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, uint32_t* indexIterator)
: m_entity(entityIterator)
, m_index(indexIterator)
{
@@ -168,7 +168,7 @@ namespace AzFramework
return *m_entity;
}
size_t SpawnableIndexEntityPair::GetIndex() const
uint32_t SpawnableIndexEntityPair::GetIndex() const
{
return *m_index;
}
@@ -177,7 +177,7 @@ namespace AzFramework
// SpawnableIndexEntityIterator
//
SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator)
SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, uint32_t* indexIterator)
: m_value(entityIterator, indexIterator)
{
}
@@ -248,7 +248,7 @@ namespace AzFramework
//
SpawnableConstIndexEntityContainerView::SpawnableConstIndexEntityContainerView(
AZ::Entity** beginEntity, size_t* beginIndices, size_t length)
AZ::Entity** beginEntity, uint32_t* beginIndices, size_t length)
: m_begin(beginEntity, beginIndices)
, m_end(beginEntity + length, beginIndices + length)
{
@@ -85,19 +85,19 @@ namespace AzFramework
AZ::Entity* GetEntity();
const AZ::Entity* GetEntity() const;
size_t GetIndex() const;
uint32_t GetIndex() const;
private:
SpawnableIndexEntityPair() = default;
SpawnableIndexEntityPair(const SpawnableIndexEntityPair&) = default;
SpawnableIndexEntityPair(SpawnableIndexEntityPair&&) = default;
SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator);
SpawnableIndexEntityPair(AZ::Entity** entityIterator, uint32_t* indexIterator);
SpawnableIndexEntityPair& operator=(const SpawnableIndexEntityPair&) = default;
SpawnableIndexEntityPair& operator=(SpawnableIndexEntityPair&&) = default;
AZ::Entity** m_entity { nullptr };
size_t* m_index { nullptr };
uint32_t* m_index { nullptr };
};
class SpawnableIndexEntityIterator
@@ -110,7 +110,7 @@ namespace AzFramework
using pointer = SpawnableIndexEntityPair*;
using reference = SpawnableIndexEntityPair&;
SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator);
SpawnableIndexEntityIterator(AZ::Entity** entityIterator, uint32_t* indexIterator);
SpawnableIndexEntityIterator& operator++();
SpawnableIndexEntityIterator operator++(int);
@@ -132,7 +132,7 @@ namespace AzFramework
class SpawnableConstIndexEntityContainerView
{
public:
SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, size_t* beginIndices, size_t length);
SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, uint32_t* beginIndices, size_t length);
const SpawnableIndexEntityIterator& begin();
const SpawnableIndexEntityIterator& end();
@@ -144,6 +144,16 @@ namespace AzFramework
SpawnableIndexEntityIterator m_end;
};
//! Information used when updating the type of an entity alias.
struct EntityAliasTypeChange
{
//! The index of the alias in the spawnable. Note that due to optimizations done on the entity aliases the index of an alias
//! can change over time.
uint32_t m_aliasIndex;
//! The type to replace type stored in the spawnable at the index provided by m_aliasIndex.
Spawnable::EntityAliasType m_newAliasType;
};
//! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that is used as a template. A ticket can
//! be reused for multiple calls on the same spawnable and is safe to be used by multiple threads at the same time. Entities created
//! from the spawnable may be tracked by the ticket and so using the same ticket is needed to despawn the exact entities created
@@ -178,6 +188,7 @@ namespace AzFramework
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using RetrieveEntitySpawnTicketCallback = AZStd::function<void(EntitySpawnTicket*)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using UpdateEntityAliasTypesCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
@@ -247,6 +258,15 @@ namespace AzFramework
SpawnablePriority m_priority { SpawnablePriority_Default };
};
struct UpdateEntityAliasTypesOptionalArgs final
{
//! Callback that's called when entity aliases are updated. This can be triggered from a different thread than the one that
//! made the function call to update.
UpdateEntityAliasTypesCallback m_completionCallback;
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
struct ListEntitiesOptionalArgs final
{
//! The priority at which this call will be executed.
@@ -265,6 +285,14 @@ namespace AzFramework
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
struct LoadBarrierOptionalArgs final
{
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
//! Also checks if the spawnables referenced in the entity aliases that are marked to be loaded are loaded.
bool m_checkAliasSpawnables{ true };
};
//! Interface definition to (de)spawn entities from a spawnable into the game world.
//!
//! While the callbacks of the individual calls are being processed they will block processing any other request. Callbacks can be
@@ -298,7 +326,7 @@ namespace AzFramework
//! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from.
//! @param optionalArgs Optional additional arguments, see SpawnEntitiesOptionalArgs.
virtual void SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
//! Removes all entities in the provided list from the environment.
//! @param ticket The ticket previously used to spawn entities with.
//! @param optionalArgs Optional additional arguments, see DespawnAllEntitiesOptionalArgs.
@@ -320,6 +348,16 @@ namespace AzFramework
virtual void ReloadSpawnable(
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) = 0;
//! Allows updating the entity alias on a spawnable. This allows the spawning behavior for all entities spawned from the used
//! spawnable to be changed and is not restricted to this ticket alone.
//! @param ticket Holds the information for the spawnable.
//! @param updateAliases An array of index and alias type values used to update the entity alias list.
//! @param optionalArgs Optional additional arguments, see UpdateEntityAliasTypesOptionalArgs.
virtual void UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs = {}) = 0;
//! List all entities that are spawned using this ticket.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param listCallback Required callback that will be called to list the entities on.
@@ -351,31 +389,37 @@ namespace AzFramework
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
//! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs.
virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) = 0;
//! Blocks until the spawnable is loaded and all operations made on the provided ticket before the barrier call have completed.
//! @param ticket The ticket to monitor.
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
//! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs.
virtual void LoadBarrier(
EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs = {}) = 0;
protected:
[[nodiscard]] virtual AZStd::pair<EntitySpawnTicket::Id, void*> CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
virtual void DestroyTicket(void* ticket) = 0;
template<typename T>
static T& GetTicketPayload(EntitySpawnTicket& ticket)
[[nodiscard]] static T& GetTicketPayload(EntitySpawnTicket& ticket)
{
return *reinterpret_cast<T*>(ticket.m_payload);
}
template<typename T>
static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
[[nodiscard]] static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
{
return *reinterpret_cast<const T*>(ticket.m_payload);
}
template<typename T>
static T* GetTicketPayload(EntitySpawnTicket* ticket)
[[nodiscard]] static T* GetTicketPayload(EntitySpawnTicket* ticket)
{
return reinterpret_cast<T*>(ticket->m_payload);
}
template<typename T>
static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
[[nodiscard]] static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
{
return reinterpret_cast<const T*>(ticket->m_payload);
}
@@ -60,7 +60,7 @@ namespace AzFramework
}
void SpawnableEntitiesManager::SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs)
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized.");
@@ -128,6 +128,20 @@ namespace AzFramework
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized.");
UpdateEntityAliasTypesCommand queueEntry;
queueEntry.m_entityAliases = AZStd::move(updatedAliases);
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback);
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs)
{
@@ -175,6 +189,19 @@ namespace AzFramework
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::LoadBarrier(
EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs)
{
AZ_Assert(completionCallback, "Load barrier on spawnable entities called without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to LoadBarrier hasn't been initialized.");
LoadBarrierCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_checkAliasSpawnables = optionalArgs.m_checkAliasSpawnables;
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
auto SpawnableEntitiesManager::ProcessQueue(CommandQueuePriority priority) -> CommandQueueStatus
{
CommandQueueStatus result = CommandQueueStatus::NoCommandsLeft;
@@ -203,13 +230,13 @@ namespace AzFramework
for (size_t i = 0; i < delayedSize; ++i)
{
Requests& request = queue.m_delayed.front();
bool result = AZStd::visit(
[this](auto&& args) -> bool
CommandResult result = AZStd::visit(
[this](auto&& args) -> CommandResult
{
return ProcessRequest(args);
},
request);
if (!result)
if (result == CommandResult::Requeue)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
@@ -230,13 +257,13 @@ namespace AzFramework
while (!pendingRequestQueue.empty())
{
Requests& request = pendingRequestQueue.front();
bool result = AZStd::visit(
[this](auto&& args) -> bool
CommandResult result = AZStd::visit(
[this](auto&& args) -> CommandResult
{
return ProcessRequest(args);
},
request);
if (!result)
if (result == CommandResult::Requeue)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
@@ -273,14 +300,73 @@ namespace AzFramework
}
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext)
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityPrototype,
EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext)
{
// If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it.
constexpr bool allowDuplicateIds = false;
return AZ::IdUtils::Remapper<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneMap, &serializeContext);
&entityPrototype, prototypeToCloneMap, &serializeContext);
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleAliasedEntity(
const AZ::Entity& entityPrototype,
const Spawnable::EntityAlias& alias,
EntityIdMap& prototypeToCloneMap,
AZ::Entity* previouslySpawnedEntity,
AZ::SerializeContext& serializeContext)
{
AZ::Entity* clone = nullptr;
switch (alias.m_aliasType)
{
case Spawnable::EntityAliasType::Original:
// Behave as the original version.
clone = CloneSingleEntity(entityPrototype, prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Disable:
// Do nothing.
return nullptr;
case Spawnable::EntityAliasType::Replace:
clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Additional:
// The asset handler will have sorted and inserted a Spawnable::EntityAliasType::Original, so the just
// spawn the additional entity.
clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Merge:
AZ_Assert(previouslySpawnedEntity != nullptr, "Merging components but there's no entity to add to yet.");
AppendComponents(
*previouslySpawnedEntity, alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetComponents(), prototypeToCloneMap,
serializeContext);
return nullptr;
default:
AZ_Assert(false, "Unsupported spawnable entity alias type: %i", alias.m_aliasType);
return nullptr;
}
}
void SpawnableEntitiesManager::AppendComponents(
AZ::Entity& target,
const AZ::Entity::ComponentArrayType& componentPrototypes,
EntityIdMap& prototypeToCloneMap,
AZ::SerializeContext& serializeContext)
{
// Only components are added and entities are looked up so no duplicate entity ids should be encountered.
constexpr bool allowDuplicateIds = false;
for (const AZ::Component* component : componentPrototypes)
{
AZ::Component* clone = AZ::IdUtils::Remapper<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
component, prototypeToCloneMap, &serializeContext);
AZ_Assert(clone, "Unable to clone component for entity '%s' (%zu).", target.GetName().c_str(), target.GetId());
[[maybe_unused]] bool result = target.AddComponent(clone);
AZ_Assert(result, "Unable to add cloned component to entity '%s' (%zu).", target.GetName().c_str(), target.GetId());
}
}
void SpawnableEntitiesManager::InitializeEntityIdMappings(
@@ -316,161 +402,264 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
// Keep track how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = entitiesToSpawn.size();
// Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
// Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference
// in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless
// of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to
// previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call.
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst();
aliases.IsValid() && aliases.AreAllSpawnablesReady())
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<uint32_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
// Keep track how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(i);
}
// These are 'prototype' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
uint32_t entitiesToSpawnSize = aznumeric_caster(entitiesToSpawn.size());
// loadAll is true if every entity has been spawned only once
ticket.m_loadAll = (spawnedEntities.size() == entitiesToSpawnSize);
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
(*it)->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
ticket.m_currentRequestId++;
return true;
}
else
{
return false;
}
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ_Assert(
spawnedEntities.size() == spawnedEntityIndices.size(),
"The indices for the spawned entities has gone out of sync with the entities.");
// Keep track of how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities)
{
// This map keeps track of ids from template (spawnable) to clone (instance) allowing patch ups of fields referring
// to entityIds outside of a given entity.
// We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// Pre-generate the full set of entity-id-to-new-entity-id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so
// that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities
// (or SpawnAllEntities) call.
// However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false".
// We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference
// in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless
// of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to
// previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call.
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
}
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
for (size_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
auto aliasIt = aliases.begin();
auto aliasEnd = aliases.end();
if (aliasIt == aliasEnd)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZ::Entity* clone =
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.push_back(clone);
spawnedEntityIndices.push_back(index);
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(i);
}
}
}
ticket.m_loadAll = false;
else
{
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != i)
{
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(i);
}
else
{
// The list of entities has already been sorted and optimized (See SpawnableEntitiesAliasList:Optimize) so can
// be safely executed in order without risking an invalid state.
AZ::Entity* previousEntity = nullptr;
do
{
AZ::Entity* clone = CloneSingleAliasedEntity(
*entitiesToSpawn[i], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity,
*request.m_serializeContext);
previousEntity = clone;
if (clone)
{
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(i);
}
++aliasIt;
} while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == i);
}
}
}
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
(*it)->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
// There were no initial entities then the ticket now holds exactly all entities. If there were already entities then
// a new set are not added so it no longer holds exactly the number of entities.
ticket.m_loadAll = spawnedEntitiesInitialCount == 0;
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
auto newEntitiesBegin = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount;
auto newEntitiesEnd = ticket.m_spawnedEntities.end();
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(newEntitiesBegin, newEntitiesEnd));
}
ticket.m_currentRequestId++;
return true;
}
else
{
return false;
// Add to the game context, now the entities are active
for (auto it = newEntitiesBegin; it != newEntitiesEnd; ++it)
{
AZ::Entity* clone = (*it);
// The entity component framework doesn't handle entities without TransformComponent safely.
if (!clone->GetComponents().empty())
{
clone->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
}
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(newEntitiesBegin, newEntitiesEnd));
}
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
}
return CommandResult::Requeue;
}
bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst();
aliases.IsValid() && aliases.AreAllSpawnablesReady())
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<uint32_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ_Assert(
spawnedEntities.size() == spawnedEntityIndices.size(),
"The indices for the spawned entities has gone out of sync with the entities.");
// Keep track of how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'prototype' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities)
{
// This map keeps track of ids from prototype (spawnable) to clone (instance) allowing patch ups of fields referring
// to entityIds outside of a given entity.
// We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so
// that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities
// (or SpawnAllEntities) call.
// However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false".
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
}
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
auto aliasBegin = aliases.begin();
auto aliasEnd = aliases.end();
if (aliasBegin == aliasEnd)
{
for (uint32_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
spawnedEntities.push_back(
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(index);
}
}
}
else
{
for (uint32_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
auto aliasIt = AZStd::lower_bound(
aliasBegin, aliasEnd, index,
[](const Spawnable::EntityAlias& lhs, uint32_t rhs)
{
return lhs.m_sourceIndex < rhs;
});
if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != index)
{
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(index);
}
else
{
// The list of entities has already been sorted and optimized (See SpawnableEntitiesAliasList:Optimize) so
// can be safely executed in order without risking an invalid state.
AZ::Entity* previousEntity = nullptr;
do
{
AZ::Entity* clone = CloneSingleAliasedEntity(
*entitiesToSpawn[index], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity,
*request.m_serializeContext);
previousEntity = clone;
if (clone)
{
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(index);
}
++aliasIt;
} while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == index);
}
}
}
}
ticket.m_loadAll = false;
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(
request.m_ticketId,
SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
AZ::Entity* clone = (*it);
// The entity component framework doesn't handle entities without TransformComponent safely.
if (!clone->GetComponents().empty())
{
clone->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
}
if (request.m_completionCallback)
{
request.m_completionCallback(
request.m_ticketId,
SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
}
return CommandResult::Requeue;
}
auto SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -495,15 +684,15 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(DespawnEntityCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(DespawnEntityCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -529,15 +718,15 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
AZ_Assert(ticket.m_spawnable.GetId() == request.m_spawnable.GetId(),
@@ -564,7 +753,7 @@ namespace AzFramework
// Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// This map is intentionally cleared out and regenerated here to ensure that we're starting fresh with mappings that
// match the new set of template entities getting spawned.
// match the new set of prototype entities getting spawned.
InitializeEntityIdMappings(entities, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
if (ticket.m_loadAll)
@@ -574,7 +763,7 @@ namespace AzFramework
ticket.m_spawnedEntityIndices.clear();
size_t entitiesToSpawnSize = entities.size();
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entities[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
@@ -590,7 +779,7 @@ namespace AzFramework
{
size_t entitiesSize = entities.size();
for (size_t index : ticket.m_spawnedEntityIndices)
for (uint32_t index : ticket.m_spawnedEntityIndices)
{
// It's possible for the new spawnable to have a different number of entities, so guard against this.
// It's also possible that the entities have moved within the spawnable to a new index. This can't be
@@ -616,15 +805,40 @@ namespace AzFramework
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(UpdateEntityAliasTypesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.IsValid())
{
for (EntityAliasTypeChange& replacement : request.m_entityAliases)
{
aliases.UpdateAliasType(replacement.m_aliasIndex, replacement.m_newAliasType);
}
aliases.Optimize();
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId);
}
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
}
return CommandResult::Requeue;
}
auto SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -632,15 +846,15 @@ namespace AzFramework
request.m_listCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -651,15 +865,15 @@ namespace AzFramework
request.m_listCallback(request.m_ticketId, SpawnableConstIndexEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntityIndices.begin(), ticket.m_spawnedEntities.size()));
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -671,15 +885,15 @@ namespace AzFramework
ticket.m_spawnedEntityIndices.clear();
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -690,15 +904,39 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(LoadBarrierCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
if (request.m_checkAliasSpawnables)
{
if (Spawnable::EntityAliasConstVisitor visitor = ticket.m_spawnable->TryGetAliasesConst();
!visitor.IsValid() || !visitor.AreAllSpawnablesReady())
{
return CommandResult::Requeue;
}
}
request.m_completionCallback(request.m_ticketId);
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
else
{
return CommandResult::Requeue;
}
}
auto SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request) -> CommandResult
{
if (request.m_requestId == request.m_ticket->m_currentRequestId)
{
@@ -706,19 +944,24 @@ namespace AzFramework
{
if (entity != nullptr)
{
// Setting it to 0 is needed to avoid the infite loop between GameEntityContext and SpawnableEntitiesManager.
// Setting it to 0 is needed to avoid the infinite loop between GameEntityContext and SpawnableEntitiesManager.
entity->SetSpawnTicketId(0);
GameEntityContextRequestBus::Broadcast(
&GameEntityContextRequestBus::Events::DestroyGameEntity, entity->GetId());
}
else
{
// Entities without components wouldn't have been send to the GameEntityContext.
delete entity;
}
}
delete request.m_ticket;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
} // namespace AzFramework
@@ -55,13 +55,18 @@ namespace AzFramework
void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
void DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void DespawnEntity(AZ::EntityId entityId, EntitySpawnTicket& ticket, DespawnEntityOptionalArgs optionalArgs = {}) override;
void RetrieveEntitySpawnTicket(EntitySpawnTicket::Id entitySpawnTicketId, RetrieveEntitySpawnTicketCallback callback) override;
void ReloadSpawnable(
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) override;
void UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs = {}) override;
void ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs = {}) override;
void ListIndicesAndEntities(
@@ -70,6 +75,8 @@ namespace AzFramework
EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs = {}) override;
void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) override;
void LoadBarrier(
EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs = {}) override;
//
// The following function is thread safe but intended to be run from the main thread.
@@ -78,14 +85,20 @@ namespace AzFramework
CommandQueueStatus ProcessQueue(CommandQueuePriority priority);
protected:
struct Ticket
enum class CommandResult : bool
{
Executed,
Requeue
};
struct Ticket final
{
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
static constexpr uint32_t Processing = AZStd::numeric_limits<uint32_t>::max();
//! Map of template entity ids to their associated instance ids.
//! Tickets can be used to spawn the same template entities multiple times, in any order, across multiple calls.
//! Since template entities can reference other entities, this map is used to fix up those references across calls
//! Map of prototype entity ids to their associated instance ids.
//! Tickets can be used to spawn the same prototype entities multiple times, in any order, across multiple calls.
//! Since prototype entities can reference other entities, this map is used to fix up those references across calls
//! using the following policy:
//! - Entities referencing an entity that hasn't been spawned yet will get a reference to the id that *will* be used
//! the first time that entity will be spawned. The reference will be invalid until that entity is spawned, but
@@ -100,14 +113,14 @@ namespace AzFramework
AZStd::unordered_set<AZ::EntityId> m_previouslySpawned;
AZStd::vector<AZ::Entity*> m_spawnedEntities;
AZStd::vector<size_t> m_spawnedEntityIndices;
AZStd::vector<uint32_t> m_spawnedEntityIndices;
AZ::Data::Asset<Spawnable> m_spawnable;
uint32_t m_nextRequestId{ 0 }; //!< Next id for this ticket.
uint32_t m_currentRequestId { 0 }; //!< The id for the command that should be executed.
bool m_loadAll{ true };
};
struct SpawnAllEntitiesCommand
struct SpawnAllEntitiesCommand final
{
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
@@ -116,9 +129,9 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct SpawnEntitiesCommand
struct SpawnEntitiesCommand final
{
AZStd::vector<size_t> m_entityIndices;
AZStd::vector<uint32_t> m_entityIndices;
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
AZ::SerializeContext* m_serializeContext;
@@ -127,7 +140,7 @@ namespace AzFramework
uint32_t m_requestId;
bool m_referencePreviouslySpawnedEntities;
};
struct DespawnAllEntitiesCommand
struct DespawnAllEntitiesCommand final
{
EntityDespawnCallback m_completionCallback;
Ticket* m_ticket;
@@ -142,7 +155,7 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ReloadSpawnableCommand
struct ReloadSpawnableCommand final
{
AZ::Data::Asset<Spawnable> m_spawnable;
ReloadSpawnableCallback m_completionCallback;
@@ -151,35 +164,51 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListEntitiesCommand
struct UpdateEntityAliasTypesCommand final
{
AZStd::vector<EntityAliasTypeChange> m_entityAliases;
UpdateEntityAliasTypesCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListEntitiesCommand final
{
ListEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListIndicesEntitiesCommand
struct ListIndicesEntitiesCommand final
{
ListIndicesEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ClaimEntitiesCommand
struct ClaimEntitiesCommand final
{
ClaimEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct BarrierCommand
struct BarrierCommand final
{
BarrierCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct DestroyTicketCommand
struct LoadBarrierCommand final
{
BarrierCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
bool m_checkAliasSpawnables;
};
struct DestroyTicketCommand final
{
Ticket* m_ticket;
uint32_t m_requestId;
@@ -191,10 +220,12 @@ namespace AzFramework
DespawnAllEntitiesCommand,
DespawnEntityCommand,
ReloadSpawnableCommand,
UpdateEntityAliasTypesCommand,
ListEntitiesCommand,
ListIndicesEntitiesCommand,
ClaimEntitiesCommand,
BarrierCommand,
LoadBarrierCommand,
DestroyTicketCommand>;
struct Queue
@@ -212,18 +243,31 @@ namespace AzFramework
CommandQueueStatus ProcessQueue(Queue& queue);
AZ::Entity* CloneSingleEntity(
const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext);
const AZ::Entity& entityPrototype, EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext);
AZ::Entity* CloneSingleAliasedEntity(
const AZ::Entity& entityPrototype,
const Spawnable::EntityAlias& alias,
EntityIdMap& prototypeToCloneMap,
AZ::Entity* previouslySpawnedEntity,
AZ::SerializeContext& serializeContext);
void AppendComponents(
AZ::Entity& target,
const AZ::Entity::ComponentArrayType& componentPrototypes,
EntityIdMap& prototypeToCloneMap,
AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnAllEntitiesCommand& request);
bool ProcessRequest(SpawnEntitiesCommand& request);
bool ProcessRequest(DespawnAllEntitiesCommand& request);
bool ProcessRequest(DespawnEntityCommand& request);
bool ProcessRequest(ReloadSpawnableCommand& request);
bool ProcessRequest(ListEntitiesCommand& request);
bool ProcessRequest(ListIndicesEntitiesCommand& request);
bool ProcessRequest(ClaimEntitiesCommand& request);
bool ProcessRequest(BarrierCommand& request);
bool ProcessRequest(DestroyTicketCommand& request);
CommandResult ProcessRequest(SpawnAllEntitiesCommand& request);
CommandResult ProcessRequest(SpawnEntitiesCommand& request);
CommandResult ProcessRequest(DespawnAllEntitiesCommand& request);
CommandResult ProcessRequest(DespawnEntityCommand& request);
CommandResult ProcessRequest(ReloadSpawnableCommand& request);
CommandResult ProcessRequest(UpdateEntityAliasTypesCommand& request);
CommandResult ProcessRequest(ListEntitiesCommand& request);
CommandResult ProcessRequest(ListIndicesEntitiesCommand& request);
CommandResult ProcessRequest(ClaimEntitiesCommand& request);
CommandResult ProcessRequest(BarrierCommand& request);
CommandResult ProcessRequest(LoadBarrierCommand& request);
CommandResult ProcessRequest(DestroyTicketCommand& request);
//! Generate a base set of original-to-new entity ID mappings to use during spawning.
//! Since Entity references get fixed up on an entity-by-entity basis while spawning, it's important to have the complete
@@ -72,7 +72,7 @@ namespace AzFramework
uint64_t SpawnableSystemComponent::AssignRootSpawnable(AZ::Data::Asset<Spawnable> rootSpawnable)
{
uint64_t generation = 0;
uint32_t generation = 0;
if (m_rootSpawnableId == rootSpawnable.GetId())
{
@@ -87,16 +87,25 @@ namespace AzFramework
// Suspend and resume processing in the container that completion calls aren't received until
// everything has been setup to accept callbacks from the call.
m_rootSpawnableContainer.Reset(rootSpawnable);
m_rootSpawnableContainer.SpawnAllEntities();
generation = m_rootSpawnableContainer.GetCurrentGeneration();
AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(),
generation);
// Don't send out the alert that the root spawnable has been assigned until the spawnable itself is ready. The common
// use case is for handlers to do something with the information in the spawnable before the entities get spawned.
m_rootSpawnableContainer.Alert(
[rootSpawnable](uint32_t generation)
{
RootSpawnableNotificationBus::Broadcast(
&RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, AZStd::move(rootSpawnable), generation);
}, SpawnableEntitiesContainer::CheckIfSpawnableIsLoaded::Yes);
m_rootSpawnableContainer.SpawnAllEntities();
m_rootSpawnableContainer.Alert(
[newSpawnable = AZStd::move(rootSpawnable)](uint32_t generation)
{
RootSpawnableNotificationBus::QueueBroadcast(
&RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, newSpawnable, generation);
&RootSpawnableNotificationBus::Events::OnRootSpawnableReady, AZStd::move(newSpawnable), generation);
});
AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(), generation);
}
else
{
@@ -132,6 +141,12 @@ namespace AzFramework
AZ_TracePrintf("Spawnables", "New root spawnable '%s' assigned (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation);
}
void SpawnableSystemComponent::OnRootSpawnableReady(
[[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable, [[maybe_unused]] uint32_t generation)
{
AZ_TracePrintf("Spawnables", "Entities from new root spawnable '%s' are ready (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation);
}
void SpawnableSystemComponent::OnRootSpawnableReleased([[maybe_unused]] uint32_t generation)
{
AZ_TracePrintf("Spawnables", "Generation %i of the root spawnable has been released.\n", generation);
@@ -82,6 +82,7 @@ namespace AzFramework
//
void OnRootSpawnableAssigned(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReady(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReleased(uint32_t generation) override;
protected:
@@ -806,12 +806,20 @@ namespace AzFramework
[[maybe_unused]] float scrollDelta,
[[maybe_unused]] float deltaTime)
{
const auto pivot = m_pivotFn();
if (!pivot.has_value())
{
EndActivation();
return targetCamera;
}
if (Beginning())
{
// as the camera starts, record the camera we would like to end up as
m_nextCamera.m_offset = m_offsetFn(m_pivotFn().GetDistance(targetCamera.Translation()));
m_nextCamera.m_offset = m_offsetFn(pivot.value().GetDistance(targetCamera.Translation()));
const auto angles =
EulerAngles(AZ::Matrix3x3::CreateFromMatrix3x4(AZ::Matrix3x4::CreateLookAt(targetCamera.Translation(), m_pivotFn())));
EulerAngles(AZ::Matrix3x3::CreateFromMatrix3x4(AZ::Matrix3x4::CreateLookAt(targetCamera.Translation(), pivot.value())));
m_nextCamera.m_pitch = angles.GetX();
m_nextCamera.m_yaw = angles.GetZ();
m_nextCamera.m_pivot = targetCamera.m_pivot;
@@ -651,7 +651,7 @@ namespace AzFramework
class FocusCameraInput : public CameraInput
{
public:
using PivotFn = AZStd::function<AZ::Vector3()>;
using PivotFn = AZStd::function<AZStd::optional<AZ::Vector3>()>;
FocusCameraInput(const InputChannelId& focusChannelId, FocusOffsetFn offsetFn);
@@ -8,8 +8,9 @@
#pragma once
#include <AzFramework/Viewport/ViewportId.h>
#include <AzCore/EBus/EBus.h>
#include <AzCore/std/optional.h>
#include <AzFramework/Viewport/ViewportId.h>
namespace AZ
{
@@ -20,18 +21,15 @@ namespace AZ
namespace AzFramework
{
class ViewportRequests
: public AZ::EBusTraits
class ViewportRequests : public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = ViewportId;
static void Reflect(AZ::ReflectContext* context);
virtual ~ViewportRequests() {}
//! Gets the current camera's world to view matrix.
virtual const AZ::Matrix4x4& GetCameraViewMatrix() const = 0;
//! Sets the current camera's world to view matrix.
@@ -44,8 +42,36 @@ namespace AzFramework
virtual AZ::Transform GetCameraTransform() const = 0;
//! Convenience method, sets the camera's world to view matrix from this AZ::Transform.
virtual void SetCameraTransform(const AZ::Transform& transform) = 0;
protected:
~ViewportRequests() = default;
};
using ViewportRequestBus = AZ::EBus<ViewportRequests>;
} //namespace AzFramework
//! The additional padding around the viewport when a viewport border is active.
struct ViewportBorderPadding
{
float m_top;
float m_bottom;
float m_left;
float m_right;
};
//! For performing queries about the state of the viewport border.
class ViewportBorderRequests : public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = ViewportId;
//! Returns if a viewport border is in effect and what the current dimensions (padding) of the border are.
virtual AZStd::optional<ViewportBorderPadding> GetViewportBorderPadding() const = 0;
protected:
~ViewportBorderRequests() = default;
};
using ViewportBorderRequestBus = AZ::EBus<ViewportBorderRequests>;
} // namespace AzFramework
@@ -286,6 +286,7 @@ set(FILES
Spawnable/RootSpawnableInterface.h
Spawnable/Spawnable.cpp
Spawnable/Spawnable.h
Spawnable/SpawnableAssetBus.h
Spawnable/SpawnableAssetHandler.h
Spawnable/SpawnableAssetHandler.cpp
Spawnable/SpawnableEntitiesContainer.h
@@ -10,6 +10,8 @@
#include <AzFramework/XcbEventHandler.h>
#include <AzFramework/XcbInterface.h>
#include <xcb/xinput.h>
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
@@ -34,6 +36,31 @@ namespace AzFramework
return m_xcbConnection.get();
}
void SetEnableXInput(xcb_connection_t* connection, bool enable) override
{
struct Mask
{
xcb_input_event_mask_t head;
xcb_input_xi_event_mask_t mask;
};
const Mask mask {
/*.head=*/{
/*.device_id=*/XCB_INPUT_DEVICE_ALL_MASTER,
/*.mask_len=*/1
},
/*.mask=*/ enable ?
(xcb_input_xi_event_mask_t)(XCB_INPUT_XI_EVENT_MASK_RAW_MOTION | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_PRESS | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_RELEASE) :
(xcb_input_xi_event_mask_t)XCB_NONE
};
const xcb_setup_t* xcbSetup = xcb_get_setup(connection);
const xcb_screen_t* xcbScreen = xcb_setup_roots_iterator(xcbSetup).data;
xcb_input_xi_select_events(connection, xcbScreen->root, 1, &mask.head);
xcb_flush(connection);
}
private:
XcbUniquePtr<xcb_connection_t, xcb_disconnect> m_xcbConnection = nullptr;
};
@@ -24,6 +24,9 @@ namespace AzFramework
virtual ~XcbConnectionManager() = default;
virtual xcb_connection_t* GetXcbConnection() const = 0;
//! Enables/Disables XInput Raw Input events.
virtual void SetEnableXInput(xcb_connection_t* connection, bool enable) = 0;
};
class XcbConnectionManagerBusTraits
@@ -23,9 +23,6 @@ namespace AzFramework
virtual ~XcbEventHandler() = default;
virtual void HandleXcbEvent(xcb_generic_event_t* event) = 0;
// ATTN This is used as a workaround for RAW Input events when using the Editor.
virtual void PollSpecialEvents(){};
};
class XcbEventHandlerBusTraits : public AZ::EBusTraits
@@ -13,21 +13,68 @@
namespace AzFramework
{
xcb_window_t GetSystemCursorFocusWindow()
xcb_window_t GetSystemCursorFocusWindow(xcb_connection_t* connection)
{
void* systemCursorFocusWindow = nullptr;
AzFramework::InputSystemCursorConstraintRequestBus::BroadcastResult(
systemCursorFocusWindow, &AzFramework::InputSystemCursorConstraintRequests::GetSystemCursorConstraintWindow);
if (!systemCursorFocusWindow)
if (systemCursorFocusWindow)
{
return XCB_NONE;
return static_cast<xcb_window_t>(reinterpret_cast<uint64_t>(systemCursorFocusWindow));
}
// TODO Clang compile error because cast .... loses information. On GNU/Linux HWND is void* and on 64-bit
// machines its obviously 64 bit but we receive the window id from m_renderOverlay.winId() which is xcb_window_t 32-bit.
// EWMH-compliant window managers set the "_NET_ACTIVE_WINDOW" property
// of the X server's root window to the currently active window. This
// retrieves value of that property.
return static_cast<xcb_window_t>(reinterpret_cast<uint64_t>(systemCursorFocusWindow));
// Get the atom for the _NET_ACTIVE_WINDOW property
constexpr int propertyNameLength = 18;
xcb_generic_error_t* error = nullptr;
XcbStdFreePtr<xcb_intern_atom_reply_t> activeWindowAtom {xcb_intern_atom_reply(
connection,
xcb_intern_atom(connection, /*only_if_exists=*/ 1, propertyNameLength, "_NET_ACTIVE_WINDOW"),
&error
)};
if (!activeWindowAtom || error)
{
if (error)
{
AZ_Warning("XcbInput", false, "Retrieving _NET_ACTIVE_WINDOW atom failed : Error code %d", error->error_code);
free(error);
}
return XCB_WINDOW_NONE;
}
// Get the root window
const xcb_window_t rootWId = xcb_setup_roots_iterator(xcb_get_setup(connection)).data->root;
// Fetch the value of the root window's _NET_ACTIVE_WINDOW property
XcbStdFreePtr<xcb_get_property_reply_t> property {xcb_get_property_reply(
connection,
xcb_get_property(
/*c=*/connection,
/*_delete=*/ 0,
/*window=*/rootWId,
/*property=*/activeWindowAtom->atom,
/*type=*/XCB_ATOM_WINDOW,
/*long_offset=*/0,
/*long_length=*/1
),
&error
)};
if (!property || error)
{
if (error)
{
AZ_Warning("XcbInput", false, "Retrieving _NET_ACTIVE_WINDOW atom failed : Error code %d", error->error_code);
free(error);
}
return XCB_WINDOW_NONE;
}
return *static_cast<xcb_window_t*>(xcb_get_property_value(property.get()));
}
xcb_connection_t* XcbInputDeviceMouse::s_xcbConnection = nullptr;
@@ -39,8 +86,7 @@ namespace AzFramework
: InputDeviceMouse::Implementation(inputDevice)
, m_systemCursorState(SystemCursorState::Unknown)
, m_systemCursorPositionNormalized(0.5f, 0.5f)
, m_prevConstraintWindow(XCB_NONE)
, m_focusWindow(XCB_NONE)
, m_focusWindow(XCB_WINDOW_NONE)
, m_cursorShown(true)
{
XcbEventHandlerBus::Handler::BusConnect();
@@ -57,14 +103,14 @@ namespace AzFramework
InputDeviceMouse::Implementation* XcbInputDeviceMouse::Create(InputDeviceMouse& inputDevice)
{
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
const auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
if (!interface)
{
AZ_Warning("XcbInput", false, "XCB interface not available");
return nullptr;
}
s_xcbConnection = AzFramework::XcbConnectionManagerInterface::Get()->GetXcbConnection();
s_xcbConnection = interface->GetXcbConnection();
if (!s_xcbConnection)
{
AZ_Warning("XcbInput", false, "XCB connection not available");
@@ -126,7 +172,7 @@ namespace AzFramework
// Get window information.
const XcbStdFreePtr<xcb_get_geometry_reply_t> xcbGeometryReply{ xcb_get_geometry_reply(
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) };
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) };
if (!xcbGeometryReply)
{
@@ -137,7 +183,7 @@ namespace AzFramework
xcb_translate_coordinates(s_xcbConnection, window, s_xcbScreen->root, 0, 0);
const XcbStdFreePtr<xcb_translate_coordinates_reply_t> xkbTranslateCoordReply{ xcb_translate_coordinates_reply(
s_xcbConnection, translate_coord, NULL) };
s_xcbConnection, translate_coord, nullptr) };
if (!xkbTranslateCoordReply)
{
@@ -173,11 +219,11 @@ namespace AzFramework
for (const auto& barrier : m_activeBarriers)
{
xcb_void_cookie_t cookie = xcb_xfixes_create_pointer_barrier_checked(
s_xcbConnection, barrier.id, window, barrier.x0, barrier.y0, barrier.x1, barrier.y1, barrier.direction, 0, NULL);
const XcbStdFreePtr<xcb_generic_error_t> xkbError{ xcb_request_check(s_xcbConnection, cookie) };
s_xcbConnection, barrier.id, window, barrier.x0, barrier.y0, barrier.x1, barrier.y1, barrier.direction, 0, nullptr);
const XcbStdFreePtr<xcb_generic_error_t> xcbError{ xcb_request_check(s_xcbConnection, cookie) };
AZ_Warning(
"XcbInput", !xkbError, "XFixes, failed to create barrier %d at (%d %d %d %d)", barrier.id, barrier.x0, barrier.y0,
"XcbInput", !xcbError, "XFixes, failed to create barrier %d at (%d %d %d %d)", barrier.id, barrier.x0, barrier.y0,
barrier.x1, barrier.y1);
}
}
@@ -207,7 +253,7 @@ namespace AzFramework
const xcb_xfixes_query_version_cookie_t query_cookie = xcb_xfixes_query_version(s_xcbConnection, 5, 0);
xcb_generic_error_t* error = NULL;
xcb_generic_error_t* error = nullptr;
const XcbStdFreePtr<xcb_xfixes_query_version_reply_t> xkbQueryRequestReply{ xcb_xfixes_query_version_reply(
s_xcbConnection, query_cookie, &error) };
@@ -244,7 +290,7 @@ namespace AzFramework
const xcb_input_xi_query_version_cookie_t query_version_cookie = xcb_input_xi_query_version(s_xcbConnection, 2, 2);
xcb_generic_error_t* error = NULL;
xcb_generic_error_t* error = nullptr;
const XcbStdFreePtr<xcb_input_xi_query_version_reply_t> xkbQueryRequestReply{ xcb_input_xi_query_version_reply(
s_xcbConnection, query_version_cookie, &error) };
@@ -268,40 +314,13 @@ namespace AzFramework
return m_xInputInitialized;
}
void XcbInputDeviceMouse::SetEnableXInput(bool enable)
{
struct
{
xcb_input_event_mask_t head;
int mask;
} mask;
mask.head.deviceid = XCB_INPUT_DEVICE_ALL;
mask.head.mask_len = 1;
if (enable)
{
mask.mask = XCB_INPUT_XI_EVENT_MASK_RAW_MOTION | XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_PRESS |
XCB_INPUT_XI_EVENT_MASK_RAW_BUTTON_RELEASE | XCB_INPUT_XI_EVENT_MASK_MOTION | XCB_INPUT_XI_EVENT_MASK_BUTTON_PRESS |
XCB_INPUT_XI_EVENT_MASK_BUTTON_RELEASE;
}
else
{
mask.mask = XCB_NONE;
}
xcb_input_xi_select_events(s_xcbConnection, s_xcbScreen->root, 1, &mask.head);
xcb_flush(s_xcbConnection);
}
void XcbInputDeviceMouse::SetSystemCursorState(SystemCursorState systemCursorState)
{
if (systemCursorState != m_systemCursorState)
{
m_systemCursorState = systemCursorState;
m_focusWindow = GetSystemCursorFocusWindow();
m_focusWindow = GetSystemCursorFocusWindow(s_xcbConnection);
HandleCursorState(m_focusWindow, systemCursorState);
}
@@ -309,52 +328,10 @@ namespace AzFramework
void XcbInputDeviceMouse::HandleCursorState(xcb_window_t window, SystemCursorState systemCursorState)
{
bool confined = false, cursorShown = true;
switch (systemCursorState)
{
case SystemCursorState::ConstrainedAndHidden:
{
//!< Constrained to the application's main window and hidden
confined = true;
cursorShown = false;
}
break;
case SystemCursorState::ConstrainedAndVisible:
{
//!< Constrained to the application's main window and visible
confined = true;
}
break;
case SystemCursorState::UnconstrainedAndHidden:
{
//!< Free to move outside the main window but hidden while inside
cursorShown = false;
}
break;
case SystemCursorState::UnconstrainedAndVisible:
{
//!< Free to move outside the application's main window and visible
}
case SystemCursorState::Unknown:
default:
break;
}
// ATTN GetSystemCursorFocusWindow when getting out of the play in editor will return XCB_NONE
// We need however the window id to reset the cursor.
if (XCB_NONE == window && (confined || cursorShown))
{
// Reuse the previous window to reset states.
window = m_prevConstraintWindow;
m_prevConstraintWindow = XCB_NONE;
}
else
{
// Remember the window we used to modify cursor and barrier states.
m_prevConstraintWindow = window;
}
SetEnableXInput(!cursorShown);
const bool confined = (systemCursorState == SystemCursorState::ConstrainedAndHidden) ||
(systemCursorState == SystemCursorState::ConstrainedAndVisible);
const bool cursorShown = (systemCursorState == SystemCursorState::ConstrainedAndVisible) ||
(systemCursorState == SystemCursorState::UnconstrainedAndVisible);
CreateBarriers(window, confined);
ShowCursor(window, cursorShown);
@@ -368,26 +345,26 @@ namespace AzFramework
void XcbInputDeviceMouse::SetSystemCursorPositionNormalizedInternal(xcb_window_t window, AZ::Vector2 positionNormalized)
{
// TODO Basically not done at all. Added only the basic functions needed.
const XcbStdFreePtr<xcb_get_geometry_reply_t> xkbGeometryReply{ xcb_get_geometry_reply(
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) };
const XcbStdFreePtr<xcb_get_geometry_reply_t> xcbGeometryReply{ xcb_get_geometry_reply(
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) };
if (!xkbGeometryReply)
if (!xcbGeometryReply)
{
return;
}
const int16_t x = static_cast<int16_t>(positionNormalized.GetX() * xkbGeometryReply->width);
const int16_t y = static_cast<int16_t>(positionNormalized.GetY() * xkbGeometryReply->height);
const int16_t x = static_cast<int16_t>(positionNormalized.GetX() * xcbGeometryReply->width);
const int16_t y = static_cast<int16_t>(positionNormalized.GetY() * xcbGeometryReply->height);
xcb_warp_pointer(s_xcbConnection, XCB_NONE, window, 0, 0, 0, 0, x, y);
xcb_warp_pointer(s_xcbConnection, XCB_WINDOW_NONE, window, 0, 0, 0, 0, x, y);
xcb_flush(s_xcbConnection);
}
void XcbInputDeviceMouse::SetSystemCursorPositionNormalized(AZ::Vector2 positionNormalized)
{
const xcb_window_t window = GetSystemCursorFocusWindow();
if (XCB_NONE == window)
const xcb_window_t window = GetSystemCursorFocusWindow(s_xcbConnection);
if (XCB_WINDOW_NONE == window)
{
return;
}
@@ -401,7 +378,7 @@ namespace AzFramework
const xcb_query_pointer_cookie_t pointer = xcb_query_pointer(s_xcbConnection, window);
const XcbStdFreePtr<xcb_query_pointer_reply_t> xkbQueryPointerReply{ xcb_query_pointer_reply(s_xcbConnection, pointer, NULL) };
const XcbStdFreePtr<xcb_query_pointer_reply_t> xkbQueryPointerReply{ xcb_query_pointer_reply(s_xcbConnection, pointer, nullptr) };
if (!xkbQueryPointerReply)
{
@@ -409,7 +386,7 @@ namespace AzFramework
}
const XcbStdFreePtr<xcb_get_geometry_reply_t> xkbGeometryReply{ xcb_get_geometry_reply(
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), NULL) };
s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) };
if (!xkbGeometryReply)
{
@@ -429,8 +406,8 @@ namespace AzFramework
AZ::Vector2 XcbInputDeviceMouse::GetSystemCursorPositionNormalized() const
{
const xcb_window_t window = GetSystemCursorFocusWindow();
if (XCB_NONE == window)
const xcb_window_t window = GetSystemCursorFocusWindow(s_xcbConnection);
if (XCB_WINDOW_NONE == window)
{
return AZ::Vector2::CreateZero();
}
@@ -455,11 +432,11 @@ namespace AzFramework
cookie = xcb_xfixes_hide_cursor_checked(s_xcbConnection, window);
}
const XcbStdFreePtr<xcb_generic_error_t> xkbError{ xcb_request_check(s_xcbConnection, cookie) };
const XcbStdFreePtr<xcb_generic_error_t> xcbError{ xcb_request_check(s_xcbConnection, cookie) };
if (xkbError)
if (xcbError)
{
AZ_Warning("XcbInput", false, "ShowCursor failed: %d", xkbError->error_code);
AZ_Warning("XcbInput", false, "ShowCursor failed: %d", xcbError->error_code);
return;
}
@@ -500,14 +477,6 @@ namespace AzFramework
}
}
void XcbInputDeviceMouse::HandlePointerMotionEvents(const xcb_generic_event_t* event)
{
const xcb_input_motion_event_t* mouseMotionEvent = reinterpret_cast<const xcb_input_motion_event_t*>(event);
m_systemCursorPosition[0] = mouseMotionEvent->event_x;
m_systemCursorPosition[1] = mouseMotionEvent->event_y;
}
void XcbInputDeviceMouse::HandleRawInputEvents(const xcb_ge_generic_event_t* event)
{
const xcb_ge_generic_event_t* genericEvent = reinterpret_cast<const xcb_ge_generic_event_t*>(event);
@@ -552,78 +521,20 @@ namespace AzFramework
}
}
void XcbInputDeviceMouse::PollSpecialEvents()
{
while (xcb_generic_event_t* genericEvent = xcb_poll_for_queued_event(s_xcbConnection))
{
// TODO Is the following correct? If we are showing the cursor, don't poll RAW Input events.
switch (genericEvent->response_type & ~0x80)
{
case XCB_GE_GENERIC:
{
const xcb_ge_generic_event_t* geGenericEvent = reinterpret_cast<const xcb_ge_generic_event_t*>(genericEvent);
// Only handle raw inputs if we have focus.
// Handle Raw Input events first.
if ((geGenericEvent->event_type == XCB_INPUT_RAW_BUTTON_PRESS) ||
(geGenericEvent->event_type == XCB_INPUT_RAW_BUTTON_RELEASE) ||
(geGenericEvent->event_type == XCB_INPUT_RAW_MOTION))
{
HandleRawInputEvents(geGenericEvent);
free(genericEvent);
}
}
break;
}
}
}
void XcbInputDeviceMouse::HandleXcbEvent(xcb_generic_event_t* event)
{
switch (event->response_type & ~0x80)
{
// QT5 is using by default XInput which means we do need to check for XCB_GE_GENERIC event to parse all mouse related events.
// XInput raw events are sent from the server as a XCB_GE_GENERIC
// event. A XCB_GE_GENERIC event is typecast to a
// xcb_ge_generic_event_t, which is distinct from a
// xcb_generic_event_t, and exists so that X11 extensions can extend
// the event emission beyond the size that a normal X11 event could
// contain.
case XCB_GE_GENERIC:
{
const xcb_ge_generic_event_t* genericEvent = reinterpret_cast<const xcb_ge_generic_event_t*>(event);
// Handling RAW Inputs here works in GameMode but not in Editor mode because QT is
// not handling RAW input events and passing to.
if (!m_cursorShown)
{
// Handle Raw Input events first.
if ((genericEvent->event_type == XCB_INPUT_RAW_BUTTON_PRESS) ||
(genericEvent->event_type == XCB_INPUT_RAW_BUTTON_RELEASE) || (genericEvent->event_type == XCB_INPUT_RAW_MOTION))
{
HandleRawInputEvents(genericEvent);
}
}
else
{
switch (genericEvent->event_type)
{
case XCB_INPUT_BUTTON_PRESS:
{
const xcb_input_button_press_event_t* mouseButtonEvent =
reinterpret_cast<const xcb_input_button_press_event_t*>(genericEvent);
HandleButtonPressEvents(mouseButtonEvent->detail, true);
}
break;
case XCB_INPUT_BUTTON_RELEASE:
{
const xcb_input_button_release_event_t* mouseButtonEvent =
reinterpret_cast<const xcb_input_button_release_event_t*>(genericEvent);
HandleButtonPressEvents(mouseButtonEvent->detail, false);
}
break;
case XCB_INPUT_MOTION:
{
HandlePointerMotionEvents(event);
}
break;
}
}
HandleRawInputEvents(genericEvent);
}
break;
case XCB_FOCUS_IN:
@@ -634,6 +545,9 @@ namespace AzFramework
m_focusWindow = focusInEvent->event;
HandleCursorState(m_focusWindow, m_systemCursorState);
}
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
interface->SetEnableXInput(interface->GetXcbConnection(), true);
}
break;
case XCB_FOCUS_OUT:
@@ -644,7 +558,10 @@ namespace AzFramework
ProcessRawEventQueues();
ResetInputChannelStates();
m_focusWindow = XCB_NONE;
m_focusWindow = XCB_WINDOW_NONE;
auto* interface = AzFramework::XcbConnectionManagerInterface::Get();
interface->SetEnableXInput(interface->GetXcbConnection(), false);
}
break;
}
@@ -65,9 +65,6 @@ namespace AzFramework
//! \ref AzFramework::InputDeviceMouse::Implementation::TickInputDevice
void TickInputDevice() override;
//! This method is called by the Editor to accommodate some events with the Editor. Never called in Game mode.
void PollSpecialEvents() override;
//! Handle X11 events.
void HandleXcbEvent(xcb_generic_event_t* event) override;
@@ -77,9 +74,6 @@ namespace AzFramework
//! Initialize XInput extension. Used for raw input during confinement and showing/hiding the cursor.
static bool InitializeXInput();
//! Enables/Disables XInput Raw Input events.
void SetEnableXInput(bool enable);
//! Create barriers.
void CreateBarriers(xcb_window_t window, bool create);
@@ -98,9 +92,6 @@ namespace AzFramework
//! Handle button press/release events.
void HandleButtonPressEvents(uint32_t detail, bool pressed);
//! Handle motion notify events.
void HandlePointerMotionEvents(const xcb_generic_event_t* event);
//! Will set cursor states and confinement modes.
void HandleCursorState(xcb_window_t window, SystemCursorState systemCursorState);
@@ -160,7 +151,6 @@ namespace AzFramework
AZ::Vector2 m_cursorHiddenPosition;
AZ::Vector2 m_systemCursorPositionNormalized;
uint32_t m_systemCursorPosition[MAX_XI_RAW_AXIS];
static xcb_connection_t* s_xcbConnection;
static xcb_screen_t* s_xcbScreen;
@@ -171,9 +161,6 @@ namespace AzFramework
//! Will be true if the xinput2 extension could be initialized.
static bool m_xInputInitialized;
//! The window that had focus
xcb_window_t m_prevConstraintWindow;
//! The current window that has focus
xcb_window_t m_focusWindow;
@@ -9,19 +9,71 @@
#include <AzCore/IO/Path/Path.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/std/containers/array.h>
#include <AzCore/std/tuple.h>
#include <errno.h>
#include <cerrno>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <unistd.h>
AZ_CVAR(bool, ap_tether_lifetime, true, nullptr, AZ::ConsoleFunctorFlags::Null,
"If enabled, a parent process that launches the AP will terminate the AP on exit");
namespace AzFramework::AssetSystem::Platform
{
void AllowAssetProcessorToForeground()
{}
[[noreturn]] static void LaunchAssetProcessorDirectly(const AZ::IO::FixedMaxPath& assetProcessorPath, AZStd::string_view engineRoot, AZStd::string_view projectPath)
{
AZStd::fixed_vector<const char*, 5> args {
assetProcessorPath.c_str(),
"--start-hidden",
};
// Add the engine path to the launch command if not empty
AZ::IO::FixedMaxPathString engineRootArg;
if (!engineRoot.empty())
{
// No need to quote these paths, this code calls exec directly and
// does not go through shell string interpolation
engineRootArg = AZ::IO::FixedMaxPathString{"--engine-path="} + AZ::IO::FixedMaxPathString{engineRoot};
args.push_back(engineRootArg.data());
}
// Add the active project path to the launch command if not empty
AZ::IO::FixedMaxPathString projectPathArg;
if (!projectPath.empty())
{
projectPathArg = AZ::IO::FixedMaxPathString{"--regset=/Amazon/AzCore/Bootstrap/project_path="} + AZ::IO::FixedMaxPathString{projectPath};
args.push_back(projectPathArg.data());
}
// Make sure this is at the end
args.push_back(nullptr); // argv itself needs to be null-terminated
execv(args[0], const_cast<char**>(args.data()));
// exec* family of functions only return on error
fprintf(stderr, "Asset Processor failed with error: %s\n", strerror(errno));
_exit(1);
}
static pid_t LaunchAssetProcessorDaemonized(const AZ::IO::FixedMaxPath& assetProcessorPath, AZStd::string_view engineRoot, AZStd::string_view projectPath)
{
// detach the child from parent
setsid();
const pid_t secondChildPid = fork();
if (secondChildPid == 0)
{
LaunchAssetProcessorDirectly(assetProcessorPath, engineRoot, projectPath);
}
return secondChildPid;
}
bool LaunchAssetProcessor(AZStd::string_view executableDirectory, AZStd::string_view engineRoot,
AZStd::string_view projectPath)
{
@@ -40,7 +92,8 @@ namespace AzFramework::AssetSystem::Platform
}
}
pid_t firstChildPid = fork();
const pid_t parentPid = getpid();
const pid_t firstChildPid = fork();
if (firstChildPid == 0)
{
// redirect output to dev/null so it doesn't hijack an existing console window
@@ -53,51 +106,33 @@ namespace AzFramework::AssetSystem::Platform
AZ::IO::FileDescriptorRedirector stderrRedirect(STDERR_FILENO);
stderrRedirect.RedirectTo(devNull, mode);
// detach the child from parent
setsid();
pid_t secondChildPid = fork();
if (secondChildPid == 0)
if (ap_tether_lifetime)
{
AZStd::array args {
assetProcessorPath.c_str(), assetProcessorPath.c_str(), "--start-hidden",
static_cast<const char*>(nullptr), static_cast<const char*>(nullptr), static_cast<const char*>(nullptr)
};
int optionalArgPos = 3;
// Add the engine path to the launch command if not empty
AZ::IO::FixedMaxPathString engineRootArg;
if (!engineRoot.empty())
prctl(PR_SET_PDEATHSIG, SIGTERM);
if (getppid() != parentPid)
{
engineRootArg = AZ::IO::FixedMaxPathString::format(R"(--engine-path="%.*s")",
aznumeric_cast<int>(engineRoot.size()), engineRoot.data());
args[optionalArgPos++] = engineRootArg.data();
_exit(1);
}
LaunchAssetProcessorDirectly(assetProcessorPath, engineRoot, projectPath);
}
else
{
const pid_t secondChildPid = LaunchAssetProcessorDaemonized(assetProcessorPath, engineRoot, projectPath);
stdoutRedirect.Reset();
stderrRedirect.Reset();
// Add the active project path to the launch command if not empty
AZ::IO::FixedMaxPathString projectPathArg;
if (!projectPath.empty())
{
projectPathArg = AZ::IO::FixedMaxPathString::format(R"(--regset="/Amazon/AzCore/Bootstrap/project_path=%.*s")",
aznumeric_cast<int>(projectPath.size()), projectPath.data());
args[optionalArgPos++] = projectPathArg.data();
}
AZStd::apply(execl, args);
// exec* family of functions only exit on error
AZ_Error("AssetSystemComponent", false, "Asset Processor failed with error: %s", strerror(errno));
_exit(1);
// exit the transient child with proper return code
int ret = (secondChildPid < 0) ? 1 : 0;
_exit(ret);
}
stdoutRedirect.Reset();
stderrRedirect.Reset();
// exit the transient child with proper return code
int ret = (secondChildPid < 0) ? 1 : 0;
_exit(ret);
}
else if (firstChildPid > 0)
{
if (ap_tether_lifetime)
{
return true;
}
// wait for first child to exit to ensure the second child was started
int status = 0;
pid_t ret = waitpid(firstChildPid, &status, 0);
@@ -106,4 +141,4 @@ namespace AzFramework::AssetSystem::Platform
return false;
}
}
} // namespace AzFramework::AssetSystem::Platform
@@ -11,6 +11,7 @@
#include <AzCore/Module/DynamicModuleHandle.h>
#include <AzCore/PlatformIncl.h>
#include <AzCore/std/containers/array.h>
#include <AzCore/std/string/conversions.h>
namespace AzFramework
@@ -234,14 +235,14 @@ namespace AzFramework
const UINT rawInputHeaderSize = sizeof(RAWINPUTHEADER);
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &rawInputSize, rawInputHeaderSize);
LPBYTE rawInputBytes = new BYTE[rawInputSize];
AZStd::array<BYTE, sizeof(RAWINPUT)> rawInputBytesArray;
LPBYTE rawInputBytes = rawInputBytesArray.data();
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, rawInputBytes, &rawInputSize, rawInputHeaderSize);
RAWINPUT* rawInput = (RAWINPUT*)rawInputBytes;
AzFramework::RawInputNotificationBusWindows::Broadcast(
&AzFramework::RawInputNotificationBusWindows::Events::OnRawInputEvent, *rawInput);
delete [] rawInputBytes;
break;
}
case WM_CHAR:
@@ -41,7 +41,9 @@ namespace UnitTest
auto projectPathKey =
AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
registry->Set(projectPathKey, "AutomatedTesting");
AZ::IO::FixedMaxPath enginePath;
registry->Get(enginePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
registry->Set(projectPathKey, (enginePath / "AutomatedTesting").Native());
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
m_application->Start({});
@@ -45,7 +45,9 @@ namespace UnitTest
auto projectPathKey =
AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
registry->Set(projectPathKey, "AutomatedTesting");
AZ::IO::FixedMaxPath enginePath;
registry->Get(enginePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
registry->Set(projectPathKey, (enginePath / "AutomatedTesting").Native());
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
m_application->Start({});
@@ -305,7 +305,9 @@ namespace UnitTest
AZ::SettingsRegistryInterface* registry = AZ::SettingsRegistry::Get();
auto projectPathKey =
AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
registry->Set(projectPathKey, "AutomatedTesting");
AZ::IO::FixedMaxPath enginePath;
registry->Get(enginePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
registry->Set(projectPathKey, (enginePath / "AutomatedTesting").Native());
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
AZ::ComponentApplication::StartupParameters startupParameters;
@@ -48,6 +48,24 @@ namespace InputUnitTests
AZStd::unique_ptr<InputSystemComponent> m_inputSystemComponent;
};
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputChannelId_ConstExpression_CopyConstructorSuccessfull)
{
constexpr InputChannelId testInputChannelId1("TestInputChannelId");
constexpr InputChannelId testInputChannelId2(testInputChannelId1);
static_assert(testInputChannelId1 == testInputChannelId2);
EXPECT_EQ(testInputChannelId1, testInputChannelId2);
}
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputDeviceId_ConstExpression_CopyConstructorSuccessfull)
{
constexpr InputDeviceId testInputDeviceId1("TestInputDeviceId");
constexpr InputDeviceId testInputDeviceId2(testInputDeviceId1);
static_assert(testInputDeviceId1 == testInputDeviceId2);
EXPECT_EQ(testInputDeviceId1, testInputDeviceId2);
}
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputContext_InitWithDataStruct_InitializationSuccessfull)
{
@@ -36,7 +36,7 @@ namespace AzFramework
MOCK_METHOD3(
SpawnEntities,
void(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs));
void(EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs));
MOCK_METHOD2(DespawnAllEntities, void(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs));
@@ -49,6 +49,13 @@ namespace AzFramework
ReloadSpawnable,
void(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs));
MOCK_METHOD3(
UpdateEntityAliasTypes,
void(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs));
MOCK_METHOD3(
ListEntities, void(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs));
@@ -61,6 +68,7 @@ namespace AzFramework
void(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs));
MOCK_METHOD3(Barrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs));
MOCK_METHOD3(LoadBarrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs));
MOCK_METHOD1(CreateTicket, AZStd::pair<EntitySpawnTicket::Id, void*>(AZ::Data::Asset<Spawnable>&& spawnable));
MOCK_METHOD1(DestroyTicket, void(void* ticket));
@@ -11,6 +11,13 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_0_VALUE_PARAMS()) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{};
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_1_VALUE_PARAMS(p0)) {
@@ -25,3 +32,38 @@ ACTION_TEMPLATE(ReturnMalloc,
*value = T{ p0, p1 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_3_VALUE_PARAMS(p0, p1, p2)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_4_VALUE_PARAMS(p0, p1, p2, p3)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2, p3 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_5_VALUE_PARAMS(p0, p1, p2, p3, p4)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2, p3, p4 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_6_VALUE_PARAMS(p0, p1, p2, p3, p4, p5)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2, p3, p4, p5 };
return value;
}
ACTION_TEMPLATE(ReturnMalloc,
HAS_1_TEMPLATE_PARAMS(typename, T),
AND_7_VALUE_PARAMS(p0, p1, p2, p3, p4, p5, p6)) {
T* value = static_cast<T*>(malloc(sizeof(T)));
*value = T{ p0, p1, p2, p3, p4, p5, p6 };
return value;
}
@@ -32,6 +32,82 @@ xcb_generic_error_t* xcb_request_check(xcb_connection_t* c, xcb_void_cookie_t co
{
return MockXcbInterface::Instance()->xcb_request_check(c, cookie);
}
const xcb_setup_t* xcb_get_setup(xcb_connection_t *c)
{
return MockXcbInterface::Instance()->xcb_get_setup(c);
}
xcb_screen_iterator_t xcb_setup_roots_iterator(const xcb_setup_t* R)
{
return MockXcbInterface::Instance()->xcb_setup_roots_iterator(R);
}
const xcb_query_extension_reply_t* xcb_get_extension_data(xcb_connection_t* c, xcb_extension_t* ext)
{
return MockXcbInterface::Instance()->xcb_get_extension_data(c, ext);
}
int xcb_flush(xcb_connection_t *c)
{
return MockXcbInterface::Instance()->xcb_flush(c);
}
xcb_query_pointer_cookie_t xcb_query_pointer(xcb_connection_t* c, xcb_window_t window)
{
return MockXcbInterface::Instance()->xcb_query_pointer(c, window);
}
xcb_query_pointer_reply_t* xcb_query_pointer_reply(xcb_connection_t* c, xcb_query_pointer_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_query_pointer_reply(c, cookie, e);
}
xcb_get_geometry_cookie_t xcb_get_geometry(xcb_connection_t* c, xcb_drawable_t drawable)
{
return MockXcbInterface::Instance()->xcb_get_geometry(c, drawable);
}
xcb_get_geometry_reply_t* xcb_get_geometry_reply(xcb_connection_t* c, xcb_get_geometry_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_get_geometry_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_warp_pointer(
xcb_connection_t* c,
xcb_window_t src_window,
xcb_window_t dst_window,
int16_t src_x,
int16_t src_y,
uint16_t src_width,
uint16_t src_height,
int16_t dst_x,
int16_t dst_y)
{
return MockXcbInterface::Instance()->xcb_warp_pointer(c, src_window, dst_window, src_x, src_y, src_width, src_height, dst_x, dst_y);
}
xcb_intern_atom_cookie_t xcb_intern_atom(xcb_connection_t* c, uint8_t only_if_exists, uint16_t name_len, const char* name)
{
return MockXcbInterface::Instance()->xcb_intern_atom(c, only_if_exists, name_len, name);
}
xcb_intern_atom_reply_t* xcb_intern_atom_reply(xcb_connection_t* c, xcb_intern_atom_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_intern_atom_reply(c, cookie, e);
}
xcb_get_property_cookie_t xcb_get_property(
xcb_connection_t* c,
uint8_t _delete,
xcb_window_t window,
xcb_atom_t property,
xcb_atom_t type,
uint32_t long_offset,
uint32_t long_length)
{
return MockXcbInterface::Instance()->xcb_get_property(c, _delete, window, property, type, long_offset, long_length);
}
xcb_get_property_reply_t* xcb_get_property_reply(xcb_connection_t* c, xcb_get_property_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_get_property_reply(c, cookie, e);
}
void* xcb_get_property_value(const xcb_get_property_reply_t* R)
{
return MockXcbInterface::Instance()->xcb_get_property_value(R);
}
uint32_t xcb_generate_id(xcb_connection_t *c)
{
return MockXcbInterface::Instance()->xcb_generate_id(c);
}
// ----------------------------------------------------------------------------
// xcb-xkb
@@ -116,4 +192,76 @@ xkb_state_component xkb_state_update_mask(
state, depressed_mods, latched_mods, locked_mods, depressed_layout, latched_layout, locked_layout);
}
// ----------------------------------------------------------------------------
// xcb-xfixes
xcb_xfixes_query_version_cookie_t xcb_xfixes_query_version(
xcb_connection_t* c, uint32_t client_major_version, uint32_t client_minor_version)
{
return MockXcbInterface::Instance()->xcb_xfixes_query_version(c, client_major_version, client_minor_version);
}
xcb_xfixes_query_version_reply_t* xcb_xfixes_query_version_reply(
xcb_connection_t* c, xcb_xfixes_query_version_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_xfixes_query_version_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_xfixes_show_cursor_checked(xcb_connection_t* c, xcb_window_t window)
{
return MockXcbInterface::Instance()->xcb_xfixes_show_cursor_checked(c, window);
}
xcb_void_cookie_t xcb_xfixes_hide_cursor_checked(xcb_connection_t* c, xcb_window_t window)
{
return MockXcbInterface::Instance()->xcb_xfixes_hide_cursor_checked(c, window);
}
xcb_void_cookie_t xcb_xfixes_delete_pointer_barrier_checked(xcb_connection_t* c, xcb_xfixes_barrier_t barrier)
{
return MockXcbInterface::Instance()->xcb_xfixes_delete_pointer_barrier_checked(c, barrier);
}
xcb_translate_coordinates_cookie_t xcb_translate_coordinates(xcb_connection_t* c, xcb_window_t src_window, xcb_window_t dst_window, int16_t src_x, int16_t src_y)
{
return MockXcbInterface::Instance()->xcb_translate_coordinates(c, src_window, dst_window, src_x, src_y);
}
xcb_translate_coordinates_reply_t* xcb_translate_coordinates_reply(xcb_connection_t* c, xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_translate_coordinates_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_xfixes_create_pointer_barrier_checked(
xcb_connection_t* c,
xcb_xfixes_barrier_t barrier,
xcb_window_t window,
uint16_t x1,
uint16_t y1,
uint16_t x2,
uint16_t y2,
uint32_t directions,
uint16_t num_devices,
const uint16_t* devices)
{
return MockXcbInterface::Instance()->xcb_xfixes_create_pointer_barrier_checked(c, barrier, window, x1, y1, x2, y2, directions, num_devices, devices);
}
// ----------------------------------------------------------------------------
// xcb-xinput
xcb_input_xi_query_version_cookie_t xcb_input_xi_query_version(xcb_connection_t* c, uint16_t major_version, uint16_t minor_version)
{
return MockXcbInterface::Instance()->xcb_input_xi_query_version(c, major_version, minor_version);
}
xcb_input_xi_query_version_reply_t* xcb_input_xi_query_version_reply(
xcb_connection_t* c, xcb_input_xi_query_version_cookie_t cookie, xcb_generic_error_t** e)
{
return MockXcbInterface::Instance()->xcb_input_xi_query_version_reply(c, cookie, e);
}
xcb_void_cookie_t xcb_input_xi_select_events(
xcb_connection_t* c, xcb_window_t window, uint16_t num_mask, const xcb_input_event_mask_t* masks)
{
return MockXcbInterface::Instance()->xcb_input_xi_select_events(c, window, num_mask, masks);
}
int xcb_input_raw_button_press_axisvalues_length (const xcb_input_raw_button_press_event_t *R)
{
return MockXcbInterface::Instance()->xcb_input_raw_button_press_axisvalues_length(R);
}
xcb_input_fp3232_t* xcb_input_raw_button_press_axisvalues_raw(const xcb_input_raw_button_press_event_t* R)
{
return MockXcbInterface::Instance()->xcb_input_raw_button_press_axisvalues_raw(R);
}
}
@@ -18,6 +18,8 @@
#undef explicit
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-x11.h>
#include <xcb/xfixes.h>
#include <xcb/xinput.h>
#include "Printers.h"
@@ -62,6 +64,37 @@ public:
MOCK_CONST_METHOD1(xcb_disconnect, void(xcb_connection_t* c));
MOCK_CONST_METHOD1(xcb_poll_for_event, xcb_generic_event_t*(xcb_connection_t* c));
MOCK_CONST_METHOD2(xcb_request_check, xcb_generic_error_t*(xcb_connection_t* c, xcb_void_cookie_t cookie));
MOCK_CONST_METHOD1(xcb_get_setup, const xcb_setup_t*(xcb_connection_t *c));
MOCK_CONST_METHOD1(xcb_setup_roots_iterator, xcb_screen_iterator_t(const xcb_setup_t* R));
MOCK_CONST_METHOD2(xcb_get_extension_data, const xcb_query_extension_reply_t*(xcb_connection_t* c, xcb_extension_t* ext));
MOCK_CONST_METHOD1(xcb_flush, int(xcb_connection_t *c));
MOCK_CONST_METHOD2(xcb_query_pointer, xcb_query_pointer_cookie_t(xcb_connection_t* c, xcb_window_t window));
MOCK_CONST_METHOD3(xcb_query_pointer_reply, xcb_query_pointer_reply_t*(xcb_connection_t* c, xcb_query_pointer_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD2(xcb_get_geometry, xcb_get_geometry_cookie_t(xcb_connection_t* c, xcb_drawable_t drawable));
MOCK_CONST_METHOD3(xcb_get_geometry_reply, xcb_get_geometry_reply_t*(xcb_connection_t* c, xcb_get_geometry_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD9(xcb_warp_pointer, xcb_void_cookie_t(
xcb_connection_t* c,
xcb_window_t src_window,
xcb_window_t dst_window,
int16_t src_x,
int16_t src_y,
uint16_t src_width,
uint16_t src_height,
int16_t dst_x,
int16_t dst_y));
MOCK_CONST_METHOD4(xcb_intern_atom, xcb_intern_atom_cookie_t(xcb_connection_t* c, uint8_t only_if_exists, uint16_t name_len, const char* name));
MOCK_CONST_METHOD3(xcb_intern_atom_reply, xcb_intern_atom_reply_t*(xcb_connection_t* c, xcb_intern_atom_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD7(xcb_get_property, xcb_get_property_cookie_t(
xcb_connection_t* c,
uint8_t _delete,
xcb_window_t window,
xcb_atom_t property,
xcb_atom_t type,
uint32_t long_offset,
uint32_t long_length));
MOCK_CONST_METHOD3(xcb_get_property_reply, xcb_get_property_reply_t*(xcb_connection_t* c, xcb_get_property_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD1(xcb_get_property_value, void*(const xcb_get_property_reply_t* R));
MOCK_CONST_METHOD1(xcb_generate_id, uint32_t(xcb_connection_t *c));
// xcb-xkb
MOCK_CONST_METHOD3(xcb_xkb_use_extension, xcb_xkb_use_extension_cookie_t(xcb_connection_t* c, uint16_t wantedMajor, uint16_t wantedMinor));
@@ -83,6 +116,33 @@ public:
MOCK_CONST_METHOD4(xkb_state_key_get_utf8, int(xkb_state* state, xkb_keycode_t key, char* buffer, size_t size));
MOCK_CONST_METHOD7(xkb_state_update_mask, xkb_state_component(xkb_state* state, xkb_mod_mask_t depressed_mods, xkb_mod_mask_t latched_mods, xkb_mod_mask_t locked_mods, xkb_layout_index_t depressed_layout, xkb_layout_index_t latched_layout, xkb_layout_index_t locked_layout));
// xcb-xfixes
MOCK_CONST_METHOD3(xcb_xfixes_query_version, xcb_xfixes_query_version_cookie_t(xcb_connection_t* c, uint32_t client_major_version, uint32_t client_minor_version));
MOCK_CONST_METHOD3(xcb_xfixes_query_version_reply, xcb_xfixes_query_version_reply_t*(xcb_connection_t* c, xcb_xfixes_query_version_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD2(xcb_xfixes_show_cursor_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window));
MOCK_CONST_METHOD2(xcb_xfixes_hide_cursor_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window));
MOCK_CONST_METHOD2(xcb_xfixes_delete_pointer_barrier_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_xfixes_barrier_t barrier));
MOCK_CONST_METHOD5(xcb_translate_coordinates, xcb_translate_coordinates_cookie_t(xcb_connection_t* c, xcb_window_t src_window, xcb_window_t dst_window, int16_t src_x, int16_t src_y));
MOCK_CONST_METHOD3(xcb_translate_coordinates_reply, xcb_translate_coordinates_reply_t*(xcb_connection_t* c, xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD10(xcb_xfixes_create_pointer_barrier_checked, xcb_void_cookie_t(
xcb_connection_t* c,
xcb_xfixes_barrier_t barrier,
xcb_window_t window,
uint16_t x1,
uint16_t y1,
uint16_t x2,
uint16_t y2,
uint32_t directions,
uint16_t num_devices,
const uint16_t* devices));
// xcb-xinput
MOCK_CONST_METHOD3(xcb_input_xi_query_version, xcb_input_xi_query_version_cookie_t(xcb_connection_t* c, uint16_t major_version, uint16_t minor_version));
MOCK_CONST_METHOD3(xcb_input_xi_query_version_reply, xcb_input_xi_query_version_reply_t*(xcb_connection_t* c, xcb_input_xi_query_version_cookie_t cookie, xcb_generic_error_t** e));
MOCK_CONST_METHOD4(xcb_input_xi_select_events, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window, uint16_t num_mask, const xcb_input_event_mask_t* masks));
MOCK_CONST_METHOD1(xcb_input_raw_button_press_axisvalues_length, int(const xcb_input_raw_button_press_event_t* R));
MOCK_CONST_METHOD1(xcb_input_raw_button_press_axisvalues_raw, xcb_input_fp3232_t*(const xcb_input_raw_button_press_event_t* R));
private:
static inline MockXcbInterface* self = nullptr;
};
@@ -22,6 +22,12 @@ namespace AzFramework
public:
void SetUp() override;
template<typename T>
static xcb_generic_event_t MakeEvent(T event)
{
return *reinterpret_cast<xcb_generic_event_t*>(&event);
}
protected:
testing::NiceMock<MockXcbInterface> m_interface;
xcb_connection_t m_connection{};
@@ -21,12 +21,6 @@
#include "XcbBaseTestFixture.h"
#include "XcbTestApplication.h"
template<typename T>
xcb_generic_event_t MakeEvent(T event)
{
return *reinterpret_cast<xcb_generic_event_t*>(&event);
}
namespace AzFramework
{
// Sets up default behavior for mock keyboard responses to xcb methods

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