merging latest development

Signed-off-by: kberg-amzn <karlberg@amazon.com>
This commit is contained in:
kberg-amzn
2021-10-21 13:45:35 -07:00
151 changed files with 4227 additions and 2078 deletions
@@ -253,73 +253,3 @@ class TestAtomEditorComponentsMain(object):
)
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_generic'])
@pytest.mark.system
class TestMaterialEditorBasicTests(object):
@pytest.fixture(autouse=True)
def setup_teardown(self, request, workspace, project):
def delete_files():
file_system.delete(
[
os.path.join(workspace.paths.project(), "Materials", "test_material.material"),
os.path.join(workspace.paths.project(), "Materials", "test_material_1.material"),
os.path.join(workspace.paths.project(), "Materials", "test_material_2.material"),
],
True,
True,
)
# Cleanup our newly created materials
delete_files()
def teardown():
# Cleanup our newly created materials
delete_files()
request.addfinalizer(teardown)
@pytest.mark.parametrize("exe_file_name", ["MaterialEditor"])
@pytest.mark.test_case_id("C34448113") # Creating a New Asset.
@pytest.mark.test_case_id("C34448114") # Opening an Existing Asset.
@pytest.mark.test_case_id("C34448115") # Closing Selected Material.
@pytest.mark.test_case_id("C34448116") # Closing All Materials.
@pytest.mark.test_case_id("C34448117") # Closing all but Selected Material.
@pytest.mark.test_case_id("C34448118") # Saving Material.
@pytest.mark.test_case_id("C34448119") # Saving as a New Material.
@pytest.mark.test_case_id("C34448120") # Saving as a Child Material.
@pytest.mark.test_case_id("C34448121") # Saving all Open Materials.
def test_MaterialEditorBasicTests(
self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name):
expected_lines = [
"Material opened: True",
"Test asset doesn't exist initially: True",
"New asset created: True",
"New Material opened: True",
"Material closed: True",
"All documents closed: True",
"Close All Except Selected worked as expected: True",
"Actual Document saved with changes: True",
"Document saved as copy is saved with changes: True",
"Document saved as child is saved with changes: True",
"Save All worked as expected: True",
]
unexpected_lines = [
# "Trace::Assert",
# "Trace::Error",
"Traceback (most recent call last):"
]
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
generic_launcher,
"hydra_AtomMaterialEditor_BasicTests.py",
run_python="--runpython",
timeout=120,
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True,
null_renderer=True,
log_file_name="MaterialEditor.log",
)
@@ -70,3 +70,75 @@ class TestAtomEditorComponentsSandbox(object):
null_renderer=True,
cfg_args=cfg_args,
)
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_generic'])
@pytest.mark.system
class TestMaterialEditorBasicTests(object):
@pytest.fixture(autouse=True)
def setup_teardown(self, request, workspace, project):
def delete_files():
file_system.delete(
[
os.path.join(workspace.paths.project(), "Materials", "test_material.material"),
os.path.join(workspace.paths.project(), "Materials", "test_material_1.material"),
os.path.join(workspace.paths.project(), "Materials", "test_material_2.material"),
],
True,
True,
)
# Cleanup our newly created materials
delete_files()
def teardown():
# Cleanup our newly created materials
delete_files()
request.addfinalizer(teardown)
@pytest.mark.parametrize("exe_file_name", ["MaterialEditor"])
@pytest.mark.test_case_id("C34448113") # Creating a New Asset.
@pytest.mark.test_case_id("C34448114") # Opening an Existing Asset.
@pytest.mark.test_case_id("C34448115") # Closing Selected Material.
@pytest.mark.test_case_id("C34448116") # Closing All Materials.
@pytest.mark.test_case_id("C34448117") # Closing all but Selected Material.
@pytest.mark.test_case_id("C34448118") # Saving Material.
@pytest.mark.test_case_id("C34448119") # Saving as a New Material.
@pytest.mark.test_case_id("C34448120") # Saving as a Child Material.
@pytest.mark.test_case_id("C34448121") # Saving all Open Materials.
def test_MaterialEditorBasicTests(
self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name):
expected_lines = [
"Material opened: True",
"Test asset doesn't exist initially: True",
"New asset created: True",
"New Material opened: True",
"Material closed: True",
"All documents closed: True",
"Close All Except Selected worked as expected: True",
"Actual Document saved with changes: True",
"Document saved as copy is saved with changes: True",
"Document saved as child is saved with changes: True",
"Save All worked as expected: True",
]
unexpected_lines = [
# "Trace::Assert",
# "Trace::Error",
"Traceback (most recent call last):"
]
hydra.launch_and_validate_results(
request,
TEST_DIRECTORY,
generic_launcher,
"hydra_AtomMaterialEditor_BasicTests.py",
run_python="--runpython",
timeout=120,
expected_lines=expected_lines,
unexpected_lines=unexpected_lines,
halt_on_unexpected=True,
null_renderer=True,
log_file_name="MaterialEditor.log",
)
@@ -9,6 +9,7 @@ from __future__ import annotations
from collections import Counter
from collections import deque
from os import path
from pathlib import Path
from PySide2 import QtWidgets
@@ -20,6 +21,10 @@ from editor_python_test_tools.utils import Report
import azlmbr.entity as entity
import azlmbr.bus as bus
import azlmbr.components as components
import azlmbr.editor as editor
import azlmbr.globals
import azlmbr.math as math
import azlmbr.prefab as prefab
import editor_python_test_tools.pyside_utils as pyside_utils
@@ -57,26 +62,46 @@ class PrefabInstance:
def __hash__(self):
return hash(self.container_entity.id)
"""
See if this instance is valid to be used with other prefab operations.
:return: Whether the target instance is valid or not.
"""
def is_valid(self) -> bool:
"""
See if this instance is valid to be used with other prefab operations.
:return: Whether the target instance is valid or not.
"""
return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs
"""
Reparent this instance to target parent entity.
The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs.
:param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next.
"""
def has_editor_prefab_component(self) -> bool:
"""
Check if the instance's container entity contains EditorPrefabComponent.
:return: Whether the container entity of target instance has EditorPrefabComponent in it or not.
"""
return editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", self.container_entity.id, azlmbr.globals.property.EditorPrefabComponentTypeId)
def is_at_position(self, expected_position):
"""
Check if the instance's container entity is at expected position given.
:return: Whether the container entity of target instance is at expected position or not.
"""
actual_position = components.TransformBus(bus.Event, "GetWorldTranslation", self.container_entity.id)
is_at_position = actual_position.IsClose(expected_position)
if not is_at_position:
Report.info(f"Prefab Instance Container Entity '{self.container_entity.id.ToString()}'\'s expected position: {expected_position.ToString()}, actual position: {actual_position.ToString()}")
return is_at_position
async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId):
"""
Reparent this instance to target parent entity.
The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs.
:param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next.
"""
container_entity_id_before_reparent = self.container_entity.id
original_parent = EditorEntity(self.container_entity.get_parent_id())
original_parent_before_reparent_children_ids = set(original_parent.get_children_ids())
original_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()}
new_parent = EditorEntity(parent_entity_id)
new_parent_before_reparent_children_ids = set(new_parent.get_children_ids())
new_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()}
pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id))
pyside_utils.run_soon(lambda: wait_for_propagation())
@@ -90,23 +115,28 @@ class PrefabInstance:
except pyside_utils.EventLoopTimeoutException:
pass
original_parent_after_reparent_children_ids = set(original_parent.get_children_ids())
original_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()}
assert len(original_parent_after_reparent_children_ids) == len(original_parent_before_reparent_children_ids) - 1, \
"The children count of the Prefab Instance's original parent should be decreased by 1."
assert not container_entity_id_before_reparent in original_parent_after_reparent_children_ids, \
"This Prefab Instance is still a child entity of its original parent entity."
new_parent_after_reparent_children_ids = set(new_parent.get_children_ids())
new_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()}
assert len(new_parent_after_reparent_children_ids) == len(new_parent_before_reparent_children_ids) + 1, \
"The children count of the Prefab Instance's new parent should be increased by 1."
container_entity_id_after_reparent = set(new_parent_after_reparent_children_ids).difference(new_parent_before_reparent_children_ids).pop()
after_before_diff = set(new_parent_after_reparent_children_ids.keys()).difference(set(new_parent_before_reparent_children_ids.keys()))
container_entity_id_after_reparent = new_parent_after_reparent_children_ids[after_before_diff.pop()]
reparented_container_entity = EditorEntity(container_entity_id_after_reparent)
reparented_container_entity_parent_id = reparented_container_entity.get_parent_id()
has_correct_parent = reparented_container_entity_parent_id.ToString() == parent_entity_id.ToString()
assert has_correct_parent, "Prefab Instance reparented is *not* under the expected parent entity"
current_instance_prefab = Prefab.get_prefab(self.prefab_file_name)
current_instance_prefab.instances.remove(self)
self.container_entity = reparented_container_entity
current_instance_prefab.instances.add(self)
# This is a helper class which contains some of the useful information about a prefab template.
class Prefab:
@@ -117,31 +147,32 @@ class Prefab:
self.file_path: str = get_prefab_file_path(file_path)
self.instances: set[PrefabInstance] = set()
"""
Check if a prefab is ready to be used to generate its instances.
:param file_path: A unique file path of the target prefab.
:return: Whether the target prefab is loaded or not.
"""
@classmethod
def is_prefab_loaded(cls, file_path: str) -> bool:
"""
Check if a prefab is ready to be used to generate its instances.
:param file_path: A unique file path of the target prefab.
:return: Whether the target prefab is loaded or not.
"""
return file_path in Prefab.existing_prefabs
"""
Check if a prefab exists in the directory for files of prefab tests.
:param file_name: A unique file name of the target prefab.
:return: Whether the target prefab exists or not.
"""
@classmethod
def prefab_exists(cls, file_path: str) -> bool:
"""
Check if a prefab exists in the directory for files of prefab tests.
:param file_name: A unique file name of the target prefab.
:return: Whether the target prefab exists or not.
"""
return path.exists(get_prefab_file_path(file_path))
"""
Return a prefab which can be used immediately.
:param file_name: A unique file name of the target prefab.
:return: The prefab with given file name.
"""
@classmethod
def get_prefab(cls, file_name: str) -> Prefab:
"""
Return a prefab which can be used immediately.
:param file_name: A unique file name of the target prefab.
:return: The prefab with given file name.
"""
assert file_name, "Received an empty file_name"
if Prefab.is_prefab_loaded(file_name):
return Prefab.existing_prefabs[file_name]
@@ -151,15 +182,15 @@ class Prefab:
Prefab.existing_prefabs[file_name] = Prefab(file_name)
return new_prefab
"""
Create a prefab in memory and return it. The very first instance of this prefab will also be created.
:param entities: The entities that should form the new prefab (along with their descendants).
:param file_name: A unique file name of new prefab.
:param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name.
:return: Created Prefab object and the very first PrefabInstance object owned by the prefab.
"""
@classmethod
def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> tuple(Prefab, PrefabInstance):
"""
Create a prefab in memory and return it. The very first instance of this prefab will also be created.
:param entities: The entities that should form the new prefab (along with their descendants).
:param file_name: A unique file name of new prefab.
:param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name.
:return: Created Prefab object and the very first PrefabInstance object owned by the prefab.
"""
assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists"
new_prefab = Prefab(file_name)
@@ -169,6 +200,9 @@ class Prefab:
container_entity_id = create_prefab_result.GetValue()
container_entity = EditorEntity(container_entity_id)
children_entity_ids = container_entity.get_children_ids()
assert len(children_entity_ids) == len(entities), f"Entity count of created prefab instance does *not* match the count of given entities."
if prefab_instance_name:
container_entity.set_name(prefab_instance_name)
@@ -180,12 +214,12 @@ class Prefab:
Prefab.existing_prefabs[file_name] = new_prefab
return new_prefab, new_prefab_instance
"""
Remove target prefab instances.
:param prefab_instances: Instances to be removed.
"""
@classmethod
def remove_prefabs(cls, prefab_instances: list[PrefabInstance]):
"""
Remove target prefab instances.
:param prefab_instances: Instances to be removed.
"""
entity_ids_to_remove = []
entity_id_queue = [prefab_instance.container_entity for prefab_instance in prefab_instances]
while entity_id_queue:
@@ -212,15 +246,89 @@ class Prefab:
instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name)
instance_deleted_prefab.instances.remove(instance)
instance = PrefabInstance()
"""
Instantiate an instance of this prefab.
:param parent_entity: The entity the prefab should be a child of in the transform hierarchy.
:param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name.
:param prefab_position: The position in world space the prefab should be instantiated in.
:return: Instantiated PrefabInstance object owned by this prefab.
"""
@classmethod
def duplicate_prefabs(cls, prefab_instances: list[PrefabInstance]):
"""
Duplicate target prefab instances.
:param prefab_instances: Instances to be duplicated.
:return: PrefabInstance objects of given prefab instances' duplicates.
"""
assert prefab_instances, "Input list of prefab instances should *not* be empty."
common_parent = EditorEntity(prefab_instances[0].container_entity.get_parent_id())
common_parent_children_ids_before_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()])
container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances]
duplicate_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DuplicateEntitiesInInstance', container_entity_ids)
assert duplicate_prefab_result.IsSuccess(), f"Prefab operation 'DuplicateEntitiesInInstance' failed. Error: {duplicate_prefab_result.GetError()}"
wait_for_propagation()
duplicate_container_entity_ids = duplicate_prefab_result.GetValue()
common_parent_children_ids_after_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()])
assert set([container_entity_id.ToString() for container_entity_id in container_entity_ids]).issubset(common_parent_children_ids_after_duplicate), \
"Provided prefab instances are *not* the children of their common parent anymore after duplication."
assert common_parent_children_ids_before_duplicate.issubset(common_parent_children_ids_after_duplicate), \
"Some children of provided entities' common parent before duplication are *not* the children of the common parent anymore after duplication."
assert len(common_parent_children_ids_after_duplicate) == len(common_parent_children_ids_before_duplicate) + len(prefab_instances), \
"The children count of the given prefab instances' common parent entity is *not* increased to the expected number."
assert EditorEntity(duplicate_container_entity_ids[0]).get_parent_id().ToString() == common_parent.id.ToString(), \
"Provided prefab instances' parent should be the same as duplicates' parent."
duplicate_instances = []
for duplicate_container_entity_id in duplicate_container_entity_ids:
prefab_file_path = prefab.PrefabPublicRequestBus(bus.Broadcast, 'GetOwningInstancePrefabPath', duplicate_container_entity_id)
assert prefab_file_path, "Returned file path should *not* be empty."
prefab_file_name = Path(prefab_file_path).stem
duplicate_instance_prefab = Prefab.get_prefab(prefab_file_name)
duplicate_instance = PrefabInstance(prefab_file_path, EditorEntity(duplicate_container_entity_id))
duplicate_instance_prefab.instances.add(duplicate_instance)
duplicate_instances.append(duplicate_instance)
return duplicate_instances
@classmethod
def detach_prefab(cls, prefab_instance: PrefabInstance):
"""
Detach target prefab instance.
:param prefab_instances: Instance to be detached.
"""
parent = EditorEntity(prefab_instance.container_entity.get_parent_id())
parent_children_ids_before_detach = set([child_id.ToString() for child_id in parent.get_children_ids()])
assert prefab_instance.has_editor_prefab_component(), f"Container entity should have EditorPrefabComponent before detachment."
detach_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DetachPrefab', prefab_instance.container_entity.id)
assert detach_prefab_result.IsSuccess(), f"Prefab operation 'DetachPrefab' failed. Error: {detach_prefab_result.GetError()}"
assert not prefab_instance.has_editor_prefab_component(), f"Container entity should *not* have EditorPrefabComponent after detachment."
parent_children_ids_after_detach = set([child_id.ToString() for child_id in parent.get_children_ids()])
assert prefab_instance.container_entity.id.ToString() in parent_children_ids_after_detach, \
"Target prefab instance's container entity id should still exists after the detachment and before the propagation."
assert len(parent_children_ids_after_detach) == len(parent_children_ids_before_detach), \
"Parent entity should still keep the same amount of children entities."
wait_for_propagation()
instance_owner_prefab = Prefab.get_prefab(prefab_instance.prefab_file_name)
instance_owner_prefab.instances.remove(prefab_instance)
prefab_instance = PrefabInstance()
def instantiate(self, parent_entity: EditorEntity=None, name: str=None, prefab_position: Vector3=Vector3()) -> PrefabInstance:
"""
Instantiate an instance of this prefab.
:param parent_entity: The entity the prefab should be a child of in the transform hierarchy.
:param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name.
:param prefab_position: The position in world space the prefab should be instantiated in.
:return: Instantiated PrefabInstance object owned by this prefab.
"""
parent_entity_id = parent_entity.id if parent_entity is not None else EntityId()
instantiate_prefab_result = prefab.PrefabPublicRequestBus(
@@ -240,4 +348,6 @@ class Prefab:
assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation."
self.instances.add(new_prefab_instance)
assert new_prefab_instance.is_at_position(prefab_position), "This prefab instance is *not* at expected position."
return new_prefab_instance
@@ -47,3 +47,11 @@ class TestAutomation(TestAutomationBase):
def test_PrefabBasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform):
from .tests import PrefabBasicWorkflow_CreateAndReparentPrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False)
def test_PrefabBasicWorkflow_CreateReparentAndDetachPrefab(self, request, workspace, editor, launcher_platform):
from .tests import PrefabBasicWorkflow_CreateReparentAndDetachPrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False)
def test_PrefabBasicWorkflow_CreateAndDuplicatePrefab(self, request, workspace, editor, launcher_platform):
from .tests import PrefabBasicWorkflow_CreateAndDuplicatePrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module)
@@ -16,16 +16,15 @@ def PrefabBasicWorkflow_CreateAndDeletePrefab():
prefab_test_utils.open_base_tests_level()
# Creates a new Entity at the root level
# Asserts if creation didn't succeed
# Creates a new entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Asserts if prefab creation doesn't succeeds
# Creates a prefab from the new entity
_, car = Prefab.create_prefab(
car_prefab_entities, CAR_PREFAB_FILE_NAME)
# Asserts if prefab deletion fails
# Deletes the prefab instance
Prefab.remove_prefabs([car])
if __name__ == "__main__":
@@ -0,0 +1,32 @@
"""
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
"""
def PrefabBasicWorkflow_CreateAndDuplicatePrefab():
CAR_PREFAB_FILE_NAME = 'car_prefab'
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.prefab_utils import Prefab
import PrefabTestUtils as prefab_test_utils
prefab_test_utils.open_base_tests_level()
# Creates a new entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Creates a prefab from the new entity
_, car = Prefab.create_prefab(
car_prefab_entities, CAR_PREFAB_FILE_NAME)
# Duplicates the prefab instance
Prefab.duplicate_prefabs([car])
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(PrefabBasicWorkflow_CreateAndDuplicatePrefab)
@@ -22,24 +22,23 @@ def PrefabBasicWorkflow_CreateAndReparentPrefab():
prefab_test_utils.open_base_tests_level()
# Creates a new Entity at the root level
# Asserts if creation didn't succeed
# Creates a new car entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Checks for prefab creation passed or not
# Creates a prefab from the car entity
_, car = Prefab.create_prefab(
car_prefab_entities, CAR_PREFAB_FILE_NAME)
# Creates another new Entity at the root level
# Creates another new wheel entity at the root level
wheel_entity = EditorEntity.create_editor_entity()
wheel_prefab_entities = [wheel_entity]
# Checks for wheel prefab creation passed or not
# Creates another prefab from the wheel entity
_, wheel = Prefab.create_prefab(
wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME)
# Checks for prefab reparenting passed or not
# Reparents the wheel prefab instance to the container entity of the car prefab instance
await wheel.ui_reparent_prefab_instance(car.container_entity.id)
run_test()
@@ -17,12 +17,11 @@ def PrefabBasicWorkflow_CreatePrefab():
prefab_test_utils.open_base_tests_level()
# Creates a new Entity at the root level
# Asserts if creation didn't succeed
# Creates a new entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Checks for prefab creation passed or not
# Creates a prefab from the new entity
Prefab.create_prefab(car_prefab_entities, CAR_PREFAB_FILE_NAME)
if __name__ == "__main__":
@@ -0,0 +1,51 @@
"""
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
"""
def PrefabBasicWorkflow_CreateReparentAndDetachPrefab():
CAR_PREFAB_FILE_NAME = 'car_prefab'
WHEEL_PREFAB_FILE_NAME = 'wheel_prefab'
import editor_python_test_tools.pyside_utils as pyside_utils
@pyside_utils.wrap_async
async def run_test():
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.prefab_utils import Prefab
import PrefabTestUtils as prefab_test_utils
prefab_test_utils.open_base_tests_level()
# Creates a new car entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Creates a prefab from the car entity
_, car = Prefab.create_prefab(
car_prefab_entities, CAR_PREFAB_FILE_NAME)
# Creates another new wheel entity at the root level
wheel_entity = EditorEntity.create_editor_entity()
wheel_prefab_entities = [wheel_entity]
# Creates another prefab from the wheel entity
_, wheel = Prefab.create_prefab(
wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME)
# Reparents the wheel prefab instance to the container entity of the car prefab instance
await wheel.ui_reparent_prefab_instance(car.container_entity.id)
# Detaches the wheel prefab instance
Prefab.detach_prefab(wheel)
run_test()
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(PrefabBasicWorkflow_CreateReparentAndDetachPrefab)
@@ -19,9 +19,8 @@ def PrefabBasicWorkflow_InstantiatePrefab():
prefab_test_utils.open_base_tests_level()
# Checks for prefab instantiation passed or not
# Instantiates a new car prefab instance
test_prefab = Prefab.get_prefab(EXISTING_TEST_PREFAB_FILE_NAME)
test_instance = test_prefab.instantiate(
prefab_position=INSTANTIATED_TEST_PREFAB_POSITION)
@@ -18,20 +18,6 @@ import azlmbr.components as components
import azlmbr.entity as entity
import azlmbr.legacy.general as general
def check_entity_at_position(entity_id, expected_entity_position):
entity_at_expected_position_result = (
"entity is at expected position",
"entity is *not* at expected position")
actual_entity_position = components.TransformBus(bus.Event, "GetWorldTranslation", entity_id)
is_at_position = actual_entity_position.IsClose(expected_entity_position)
Report.result(entity_at_expected_position_result, is_at_position)
if not is_at_position:
Report.info(f"Entity '{entity_id.ToString()}'\'s expected position: {expected_entity_position.ToString()}, actual position: {actual_entity_position.ToString()}")
return is_at_position
def check_entity_children_count(entity_id, expected_children_count):
entity_children_count_matched_result = (
"Entity with a unique name found",
@@ -47,19 +33,6 @@ def check_entity_children_count(entity_id, expected_children_count):
return entity_children_count_matched
def get_children_ids_by_name(entity_id, entity_name):
entity = EditorEntity(entity_id)
children_entity_ids = entity.get_children_ids()
result = []
for child_entity_id in children_entity_ids:
child_entity = EditorEntity(child_entity_id)
child_entity_name = child_entity.get_name()
if child_entity_name == entity_name:
result.append(child_entity_id)
return result
def open_base_tests_level():
helper.init_idle()
helper.open_level("Prefab", "Base")
@@ -9,6 +9,7 @@ import os
import pytest
import ly_test_tools.environment.file_system as file_system
import ly_test_tools._internal.pytest_plugin as internal_plugin
from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite
@@ -79,6 +80,8 @@ class TestAutomation(EditorTestSuite):
class test_LandscapeCanvas_GradientNodes_EntityRemovedOnNodeDelete(EditorSharedTest):
from .EditorScripts import GradientNodes_EntityRemovedOnNodeDelete as test_module
@pytest.mark.skipif("debug" == os.path.basename(internal_plugin.build_directory),
reason="https://github.com/o3de/o3de/issues/4872")
class test_LandscapeCanvas_GraphUpdates_UpdateComponents(EditorSharedTest):
from .EditorScripts import GraphUpdates_UpdateComponents as test_module
-923
View File
@@ -1,923 +0,0 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "EditorDefs.h"
#include "ColorGradientCtrl.h"
// Qt
#include <QPainter>
#include <QToolTip>
// AzQtComponents
#include <AzQtComponents/Components/Widgets/ColorPicker.h>
#define MIN_TIME_EPSILON 0.01f
//////////////////////////////////////////////////////////////////////////
CColorGradientCtrl::CColorGradientCtrl(QWidget* parent)
: QWidget(parent)
{
m_nActiveKey = -1;
m_nHitKeyIndex = -1;
m_nKeyDrawRadius = 3;
m_bTracking = false;
m_pSpline = nullptr;
m_fMinTime = -1;
m_fMaxTime = 1;
m_fMinValue = -1;
m_fMaxValue = 1;
m_fTooltipScaleX = 1;
m_fTooltipScaleY = 1;
m_bNoTimeMarker = true;
m_bLockFirstLastKey = false;
m_bNoZoom = true;
ClearSelection();
m_bSelectedKeys.reserve(0);
m_fTimeMarker = -10;
m_grid.zoom.x = 100;
setMouseTracking(true);
}
CColorGradientCtrl::~CColorGradientCtrl()
{
}
/////////////////////////////////////////////////////////////////////////////
// QColorGradientCtrl message handlers
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
QRect rc(QPoint(0, 0), event->size());
m_rcGradient = rc;
m_rcGradient.setHeight(m_rcGradient.height() - 11);
//m_rcGradient.DeflateRect(4,4);
m_grid.rect = m_rcGradient;
if (m_bNoZoom)
{
m_grid.zoom.x = static_cast<f32>(m_grid.rect.width());
}
m_rcKeys = rc;
m_rcKeys.setTop(m_rcKeys.bottom() - 10);
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetZoom(float fZoom)
{
m_grid.zoom.x = fZoom;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetOrigin(float fOffset)
{
m_grid.origin.x = fOffset;
}
//////////////////////////////////////////////////////////////////////////
QPoint CColorGradientCtrl::KeyToPoint(int nKey)
{
if (nKey >= 0)
{
return TimeToPoint(m_pSpline->GetKeyTime(nKey));
}
return QPoint(0, 0);
}
//////////////////////////////////////////////////////////////////////////
QPoint CColorGradientCtrl::TimeToPoint(float time)
{
return QPoint(m_grid.WorldToClient(Vec2(time, 0)).x(), m_rcGradient.height() / 2);
}
//////////////////////////////////////////////////////////////////////////
AZ::Color CColorGradientCtrl::TimeToColor(float time)
{
ISplineInterpolator::ValueType val;
m_pSpline->Interpolate(time, val);
const AZ::Color col = ValueToColor(val);
return col;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val)
{
time = XOfsToTime(point.x());
ColorToValue(TimeToColor(time), val);
}
//////////////////////////////////////////////////////////////////////////
float CColorGradientCtrl::XOfsToTime(int x)
{
return m_grid.ClientToWorld(QPoint(x, 0)).x;
}
//////////////////////////////////////////////////////////////////////////
QPoint CColorGradientCtrl::XOfsToPoint(int x)
{
return TimeToPoint(XOfsToTime(x));
}
//////////////////////////////////////////////////////////////////////////
AZ::Color CColorGradientCtrl::XOfsToColor(int x)
{
return TimeToColor(XOfsToTime(x));
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::paintEvent(QPaintEvent* e)
{
QPainter painter(this);
QRect rcClient = rect();
if (m_pSpline)
{
m_bSelectedKeys.resize(m_pSpline->GetKeyCount());
}
{
if (!isEnabled())
{
painter.setBrush(palette().button());
painter.drawRect(rcClient);
return;
}
//////////////////////////////////////////////////////////////////////////
// Fill keys backgound.
//////////////////////////////////////////////////////////////////////////
QRect rcKeys = m_rcKeys.intersected(e->rect());
painter.setBrush(palette().button());
painter.drawRect(rcKeys);
//////////////////////////////////////////////////////////////////////////
//Draw Keys and Curve
if (m_pSpline)
{
DrawGradient(e, &painter);
DrawKeys(e, &painter);
}
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::DrawGradient(QPaintEvent* e, QPainter* painter)
{
//Draw Curve
// create and select a thick, white pen
painter->setPen(QPen(QColor(128, 255, 128), 1, Qt::SolidLine));
const QRect rcClip = e->rect().intersected(m_rcGradient);
const int right = rcClip.left() + rcClip.width();
for (int x = rcClip.left(); x < right; x++)
{
const AZ::Color col = XOfsToColor(x);
QPen pen(QColor(col.GetR8(), col.GetG8(), col.GetR8(), col.GetA8()), 1, Qt::SolidLine);
painter->setPen(pen);
painter->drawLine(x, m_rcGradient.top(), x, m_rcGradient.top() + m_rcGradient.height());
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::DrawKeys(QPaintEvent* e, QPainter* painter)
{
if (!m_pSpline)
{
return;
}
// create and select a white pen
painter->setPen(QPen(QColor(0, 0, 0), 1, Qt::SolidLine));
QRect rcClip = e->rect();
m_bSelectedKeys.resize(m_pSpline->GetKeyCount());
for (int i = 0; i < m_pSpline->GetKeyCount(); i++)
{
float time = m_pSpline->GetKeyTime(i);
QPoint pt = TimeToPoint(time);
if (pt.x() < rcClip.left() - 8 || pt.x() > rcClip.left() + rcClip.width() + 8)
{
continue;
}
const AZ::Color clr = TimeToColor(time);
QBrush brush(QColor(clr.GetR8(), clr.GetG8(), clr.GetB8(), clr.GetA8()));
painter->setBrush(brush);
// Find the midpoints of the top, right, left, and bottom
// of the client area. They will be the vertices of our polygon.
QPoint pts[3];
pts[0].rx() = pt.x();
pts[0].ry() = m_rcKeys.top() + 1;
pts[1].rx() = pt.x() - 5;
pts[1].ry() = m_rcKeys.top() + 8;
pts[2].rx() = pt.x() + 5;
pts[2].ry() = m_rcKeys.top() + 8;
painter->drawPolygon(pts, 3);
if (m_bSelectedKeys[i])
{
QPen pen(QColor(200, 0, 0), 1, Qt::SolidLine);
QPen oldPen = painter->pen();
painter->setPen(pen);
painter->drawPolygon(pts, 3);
painter->setPen(oldPen);
}
}
if (!m_bNoTimeMarker)
{
QPen timePen(QColor(255, 0, 255), 1, Qt::SolidLine);
painter->setPen(timePen);
QPoint pt = TimeToPoint(m_fTimeMarker);
painter->drawLine(pt.x(), m_rcGradient.top() + 1, pt.x(), m_rcGradient.bottom() - 1);
}
}
void CColorGradientCtrl::UpdateTooltip(QPoint pos)
{
if (m_nHitKeyIndex >= 0)
{
float time = m_pSpline->GetKeyTime(m_nHitKeyIndex);
ISplineInterpolator::ValueType val;
m_pSpline->GetKeyValue(m_nHitKeyIndex, val);
AZ::Color col = TimeToColor(time);
int cont_s = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_IN_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2;
int cont_d = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_OUT_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2;
QString tipText(tr("%1 : %2,%3,%4 [%5,%6]").arg(time * m_fTooltipScaleX, 0, 'f', 2).arg(col.GetR8()).arg(col.GetG8()).arg(col.GetB8()).arg(cont_s).arg(cont_d));
const QPoint globalPos = mapToGlobal(pos);
QToolTip::showText(mapToGlobal(pos), tipText, this, QRect(globalPos, QSize(1, 1)));
}
}
/////////////////////////////////////////////////////////////////////////////
//Mouse Message Handlers
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::mousePressEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton)
{
OnLButtonDown(event);
}
else if (event->button() == Qt::RightButton)
{
OnRButtonDown(event);
}
}
void CColorGradientCtrl::OnLButtonDown([[maybe_unused]] QMouseEvent* event)
{
if (m_bTracking)
{
return;
}
if (!m_pSpline)
{
return;
}
setFocus();
switch (m_hitCode)
{
case HIT_KEY:
StartTracking();
SetActiveKey(m_nHitKeyIndex);
break;
/*
case HIT_SPLINE:
{
// Cycle the spline slope of the nearest key.
int flags = m_pSpline->GetKeyFlags(m_nHitKeyIndex);
if (m_nHitKeyDist < 0)
// Toggle left side.
flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_IN_SHIFT;
if (m_nHitKeyDist > 0)
// Toggle right side.
flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_OUT_SHIFT;
m_pSpline->SetKeyFlags(m_nHitKeyIndex, flags);
m_pSpline->Update();
SetActiveKey(-1);
SendNotifyEvent( CLRGRDN_CHANGE );
if (m_updateCallback)
m_updateCallback(this);
break;
}
*/
case HIT_NOTHING:
SetActiveKey(-1);
break;
}
update();
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::OnRButtonDown([[maybe_unused]] QMouseEvent* event)
{
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::mouseDoubleClickEvent(QMouseEvent* event)
{
if (!m_pSpline)
{
return;
}
if (event->button() != Qt::LeftButton)
{
return;
}
switch (m_hitCode)
{
case HIT_SPLINE:
{
int iIndex = InsertKey(event->pos());
SetActiveKey(iIndex);
EditKey(iIndex);
update();
}
break;
case HIT_KEY:
{
EditKey(m_nHitKeyIndex);
}
break;
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::mouseMoveEvent(QMouseEvent* event)
{
if (!m_pSpline)
{
return;
}
if (!m_bTracking)
{
switch (HitTest(event->pos()))
{
case HIT_SPLINE:
{
setCursor(CMFCUtils::LoadCursor(IDC_ARRWHITE));
} break;
case HIT_KEY:
{
setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCK));
} break;
default:
break;
}
}
if (m_bTracking)
{
TrackKey(event->pos());
}
if (m_bTracking || m_nHitKeyIndex >= 0)
{
UpdateTooltip(event->pos());
}
else
{
QToolTip::hideText();
}
}
void CColorGradientCtrl::mouseReleaseEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton)
{
OnLButtonUp(event);
}
else if (event->button() == Qt::RightButton)
{
OnRButtonUp(event);
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::OnLButtonUp(QMouseEvent* event)
{
if (!m_pSpline)
{
return;
}
if (m_bTracking)
{
StopTracking(event->pos());
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::OnRButtonUp([[maybe_unused]] QMouseEvent* event)
{
if (!m_pSpline)
{
return;
}
}
/////////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetActiveKey(int nIndex)
{
ClearSelection();
//Activate New Key
if (nIndex >= 0)
{
m_bSelectedKeys[nIndex] = true;
}
m_nActiveKey = nIndex;
update();
SendNotifyEvent(CLRGRDN_ACTIVE_KEY_CHANGE);
}
/////////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetSpline(ISplineInterpolator* pSpline, bool bRedraw)
{
if (pSpline != m_pSpline)
{
//if (pSpline && pSpline->GetNumDimensions() != 3)
//return;
m_pSpline = pSpline;
m_nActiveKey = -1;
}
ClearSelection();
if (bRedraw)
{
update();
}
}
//////////////////////////////////////////////////////////////////////////
ISplineInterpolator* CColorGradientCtrl::GetSpline()
{
return m_pSpline;
}
/////////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::keyPressEvent(QKeyEvent* event)
{
bool bProcessed = false;
if (m_nActiveKey != -1 && m_pSpline)
{
switch (event->key())
{
case Qt::Key_Delete:
{
RemoveKey(m_nActiveKey);
bProcessed = true;
} break;
case Qt::Key_Up:
{
CUndo undo("Move Spline Key");
QPoint point = KeyToPoint(m_nActiveKey);
point.rx() -= 1;
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
TrackKey(point);
bProcessed = true;
} break;
case Qt::Key_Down:
{
CUndo undo("Move Spline Key");
QPoint point = KeyToPoint(m_nActiveKey);
point.rx() += 1;
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
TrackKey(point);
bProcessed = true;
} break;
case Qt::Key_Left:
{
CUndo undo("Move Spline Key");
QPoint point = KeyToPoint(m_nActiveKey);
point.rx() -= 1;
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
TrackKey(point);
bProcessed = true;
} break;
case Qt::Key_Right:
{
CUndo undo("Move Spline Key");
QPoint point = KeyToPoint(m_nActiveKey);
point.rx() += 1;
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
TrackKey(point);
bProcessed = true;
} break;
default:
break; //do nothing
}
update();
}
event->setAccepted(bProcessed);
}
//////////////////////////////////////////////////////////////////////////////
CColorGradientCtrl::EHitCode CColorGradientCtrl::HitTest(QPoint point)
{
if (!m_pSpline)
{
return HIT_NOTHING;
}
ISplineInterpolator::ValueType val;
float time;
PointToTimeValue(point, time, val);
QRect rc = rect();
m_nHitKeyIndex = -1;
if (rc.contains(point))
{
m_nHitKeyDist = 0xFFFF;
m_hitCode = HIT_SPLINE;
for (int i = 0; i < m_pSpline->GetKeyCount(); i++)
{
QPoint splinePt = TimeToPoint(m_pSpline->GetKeyTime(i));
if (abs(point.x() - splinePt.x()) < abs(m_nHitKeyDist))
{
m_nHitKeyIndex = i;
m_nHitKeyDist = point.x() - splinePt.x();
}
}
if (abs(m_nHitKeyDist) < 4)
{
m_hitCode = HIT_KEY;
}
}
else
{
m_hitCode = HIT_NOTHING;
}
return m_hitCode;
}
///////////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::StartTracking()
{
m_bTracking = true;
GetIEditor()->BeginUndo();
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCKCROSS));
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::TrackKey(QPoint point)
{
if (point.x() < m_rcGradient.left() || point.y() > m_rcGradient.right())
{
return;
}
int nKey = m_nHitKeyIndex;
if (nKey >= 0)
{
ISplineInterpolator::ValueType val;
float time;
PointToTimeValue(point, time, val);
// Clamp to min/max time.
if (time < m_fMinTime || time > m_fMaxTime)
{
return;
}
int i;
for (i = 0; i < m_pSpline->GetKeyCount(); i++)
{
// Switch to next key.
if ((m_pSpline->GetKeyTime(i) < time && i > nKey) ||
(m_pSpline->GetKeyTime(i) > time && i < nKey))
{
m_pSpline->SetKeyTime(nKey, time);
m_pSpline->Update();
SetActiveKey(i);
m_nHitKeyIndex = i;
return;
}
}
if (!m_bLockFirstLastKey || (nKey != 0 && nKey != m_pSpline->GetKeyCount() - 1))
{
m_pSpline->SetKeyTime(nKey, time);
m_pSpline->Update();
}
SendNotifyEvent(CLRGRDN_CHANGE);
if (m_updateCallback)
{
m_updateCallback(this);
}
update();
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::StopTracking(QPoint point)
{
if (!m_bTracking)
{
return;
}
GetIEditor()->AcceptUndo("Spline Move");
if (m_nHitKeyIndex >= 0)
{
QRect rc = rect();
rc = rc.marginsAdded(QMargins(100, 100, 100, 100));
if (!rc.contains(point))
{
RemoveKey(m_nHitKeyIndex);
}
}
m_bTracking = false;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::EditKey(int nKey)
{
if (!m_pSpline)
{
return;
}
if (nKey < 0 || nKey >= m_pSpline->GetKeyCount())
{
return;
}
SetActiveKey(nKey);
ISplineInterpolator::ValueType val;
m_pSpline->GetKeyValue(nKey, val);
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
AzQtComponents::ColorPicker dlg(AzQtComponents::ColorPicker::Configuration::RGB);
dlg.setCurrentColor(ValueToColor(val));
dlg.setSelectedColor(ValueToColor(val));
connect(&dlg, &AzQtComponents::ColorPicker::currentColorChanged, this, &CColorGradientCtrl::OnKeyColorChanged);
if (dlg.exec() == QDialog::Accepted)
{
CUndo undo("Modify Gradient Color");
OnKeyColorChanged(dlg.selectedColor());
}
else
{
OnKeyColorChanged(ValueToColor(val));
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::OnKeyColorChanged(const AZ::Color& color)
{
int nKey = m_nActiveKey;
if (!m_pSpline)
{
return;
}
if (nKey < 0 || nKey >= m_pSpline->GetKeyCount())
{
return;
}
ISplineInterpolator::ValueType val;
ColorToValue(color, val);
m_pSpline->SetKeyValue(nKey, val);
update();
if (m_bLockFirstLastKey)
{
if (nKey == 0)
{
m_pSpline->SetKeyValue(m_pSpline->GetKeyCount() - 1, val);
}
else if (nKey == m_pSpline->GetKeyCount() - 1)
{
m_pSpline->SetKeyValue(0, val);
}
}
m_pSpline->Update();
SendNotifyEvent(CLRGRDN_CHANGE);
if (m_updateCallback)
{
m_updateCallback(this);
}
GetIEditor()->UpdateViews(eRedrawViewports);
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::RemoveKey(int nKey)
{
if (!m_pSpline)
{
return;
}
if (m_bLockFirstLastKey)
{
if (nKey == 0 || nKey == m_pSpline->GetKeyCount() - 1)
{
return;
}
}
CUndo undo("Remove Spline Key");
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
m_nActiveKey = -1;
m_nHitKeyIndex = -1;
if (m_pSpline)
{
m_pSpline->RemoveKey(nKey);
m_pSpline->Update();
}
SendNotifyEvent(CLRGRDN_CHANGE);
if (m_updateCallback)
{
m_updateCallback(this);
}
update();
}
//////////////////////////////////////////////////////////////////////////
int CColorGradientCtrl::InsertKey(QPoint point)
{
CUndo undo("Spline Insert Key");
ISplineInterpolator::ValueType val;
float time;
PointToTimeValue(point, time, val);
if (time < m_fMinTime || time > m_fMaxTime)
{
return -1;
}
int i;
for (i = 0; i < m_pSpline->GetKeyCount(); i++)
{
// Skip if any key already have time that is very close.
if (fabs(m_pSpline->GetKeyTime(i) - time) < MIN_TIME_EPSILON)
{
return i;
}
}
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
m_pSpline->InsertKey(time, val);
m_pSpline->Interpolate(time, val);
ClearSelection();
update();
SendNotifyEvent(CLRGRDN_CHANGE);
if (m_updateCallback)
{
m_updateCallback(this);
}
for (i = 0; i < m_pSpline->GetKeyCount(); i++)
{
// Find key with added time.
if (m_pSpline->GetKeyTime(i) == time)
{
return i;
}
}
return -1;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::ClearSelection()
{
m_nActiveKey = -1;
if (m_pSpline)
{
m_bSelectedKeys.resize(m_pSpline->GetKeyCount());
}
for (int i = 0; i < (int)m_bSelectedKeys.size(); i++)
{
m_bSelectedKeys[i] = false;
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetTimeMarker(float fTime)
{
if (!m_pSpline)
{
return;
}
{
QPoint pt = TimeToPoint(m_fTimeMarker);
QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized();
rc += QMargins(1, 0, 1, 0);
update(rc);
}
{
QPoint pt = TimeToPoint(fTime);
QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized();
rc += QMargins(1, 0, 1, 0);
update(rc);
}
m_fTimeMarker = fTime;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SendNotifyEvent(int nEvent)
{
switch (nEvent)
{
case CLRGRDN_BEFORE_CHANGE:
emit beforeChange();
break;
case CLRGRDN_CHANGE:
emit change();
break;
case CLRGRDN_ACTIVE_KEY_CHANGE:
emit activeKeyChange();
break;
}
}
//////////////////////////////////////////////////////////////////////////
AZ::Color CColorGradientCtrl::ValueToColor(ISplineInterpolator::ValueType val)
{
const AZ::Color color(val[0], val[1], val[2], 1.0);
return color.LinearToGamma();
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val)
{
const AZ::Color colLin = col.GammaToLinear();
val[0] = colLin.GetR();
val[1] = colLin.GetG();
val[2] = colLin.GetB();
val[3] = 0;
}
void CColorGradientCtrl::SetNoTimeMarker(bool noTimeMarker)
{
m_bNoTimeMarker = noTimeMarker;
update();
}
#include <Controls/moc_ColorGradientCtrl.cpp>
-167
View File
@@ -1,167 +0,0 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#ifndef CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H
#define CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H
#pragma once
#if !defined(Q_MOC_RUN)
#include <QWidget>
#include <ISplines.h>
#include "Controls/WndGridHelper.h"
#endif
namespace AZ
{
class Color;
}
// Notify event sent when spline is being modified.
#define CLRGRDN_CHANGE (0x0001)
// Notify event sent just before when spline is modified.
#define CLRGRDN_BEFORE_CHANGE (0x0002)
// Notify event sent when the active key changes
#define CLRGRDN_ACTIVE_KEY_CHANGE (0x0003)
//////////////////////////////////////////////////////////////////////////
// Spline control.
//////////////////////////////////////////////////////////////////////////
class CColorGradientCtrl
: public QWidget
{
Q_OBJECT
public:
CColorGradientCtrl(QWidget* parent = nullptr);
virtual ~CColorGradientCtrl();
//Key functions
int GetActiveKey() { return m_nActiveKey; };
void SetActiveKey(int nIndex);
int InsertKey(QPoint point);
// Turns on/off zooming and scroll support.
void SetNoZoom([[maybe_unused]] bool bNoZoom) { m_bNoZoom = false; };
void SetTimeRange(float tmin, float tmax) { m_fMinTime = tmin; m_fMaxTime = tmax; }
void SetValueRange(float tmin, float tmax) { m_fMinValue = tmin; m_fMaxValue = tmax; }
void SetTooltipValueScale(float x, float y) { m_fTooltipScaleX = x; m_fTooltipScaleY = y; };
// Lock value of first and last key to be the same.
void LockFirstAndLastKeys(bool bLock) { m_bLockFirstLastKey = bLock; }
void SetSpline(ISplineInterpolator* pSpline, bool bRedraw = false);
ISplineInterpolator* GetSpline();
void SetTimeMarker(float fTime);
// Zoom in pixels per time unit.
void SetZoom(float fZoom);
void SetOrigin(float fOffset);
typedef AZStd::function<void(CColorGradientCtrl*)> UpdateCallback;
void SetUpdateCallback(const UpdateCallback& cb) { m_updateCallback = cb; };
void SetNoTimeMarker(bool noTimeMarker);
signals:
void change();
void beforeChange();
void activeKeyChange();
protected:
enum EHitCode
{
HIT_NOTHING,
HIT_KEY,
HIT_SPLINE,
};
void paintEvent(QPaintEvent* e) override;
void resizeEvent(QResizeEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void OnLButtonDown(QMouseEvent* event);
void mouseMoveEvent(QMouseEvent* event) override;
void OnLButtonUp(QMouseEvent* event);
void OnRButtonUp(QMouseEvent* event);
void mouseDoubleClickEvent(QMouseEvent* event) override;
void OnRButtonDown(QMouseEvent* event);
void keyPressEvent(QKeyEvent* event) override;
// Drawing functions
void DrawGradient(QPaintEvent* e, QPainter* painter);
void DrawKeys(QPaintEvent* e, QPainter* painter);
void UpdateTooltip(QPoint pos);
EHitCode HitTest(QPoint point);
//Tracking support helper functions
void StartTracking();
void TrackKey(QPoint point);
void StopTracking(QPoint point);
void RemoveKey(int nKey);
void EditKey(int nKey);
QPoint KeyToPoint(int nKey);
QPoint TimeToPoint(float time);
void PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val);
float XOfsToTime(int x);
QPoint XOfsToPoint(int x);
AZ::Color XOfsToColor(int x);
AZ::Color TimeToColor(float time);
void ClearSelection();
void SendNotifyEvent(int nEvent);
AZ::Color ValueToColor(ISplineInterpolator::ValueType val);
void ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val);
private:
void OnKeyColorChanged(const AZ::Color& color);
private:
ISplineInterpolator* m_pSpline;
bool m_bNoZoom;
QRect m_rcClipRect;
QRect m_rcGradient;
QRect m_rcKeys;
QPoint m_hitPoint;
EHitCode m_hitCode;
int m_nHitKeyIndex;
int m_nHitKeyDist;
QPoint m_curvePoint;
float m_fTimeMarker;
int m_nActiveKey;
int m_nKeyDrawRadius;
bool m_bTracking;
float m_fMinTime, m_fMaxTime;
float m_fMinValue, m_fMaxValue;
float m_fTooltipScaleX, m_fTooltipScaleY;
bool m_bLockFirstLastKey;
bool m_bNoTimeMarker;
std::vector<int> m_bSelectedKeys;
UpdateCallback m_updateCallback;
CWndGridHelper m_grid;
};
#endif // CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H
@@ -27,7 +27,6 @@ void RegisterReflectedVarHandlers()
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LocalStringPropertyHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LightAnimationPropertyHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew UserPopupWidgetHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew ColorCurveHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew FloatCurveHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew MotionPropertyWidgetHandler());
}
@@ -170,30 +170,3 @@ bool FloatCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CSpline
GUI->SetSpline(reinterpret_cast<ISplineInterpolator*>(instance.m_spline));
return false;
}
QWidget* ColorCurveHandler::CreateGUI(QWidget *pParent)
{
CColorGradientCtrl* gradientCtrl = new CColorGradientCtrl(pParent);
//connect(gradientCtrl, &CColorGradientCtrl::change, [gradientCtrl]()
//{
// EBUS_EVENT(AzToolsFramework::PropertyEditorGUIMessages::Bus, RequestWrite, gradientCtrl);
//});
gradientCtrl->SetTimeRange(0, 1);
gradientCtrl->setFixedHeight(36);
return gradientCtrl;
}
void ColorCurveHandler::ConsumeAttribute(CColorGradientCtrl*, AZ::u32, AzToolsFramework::PropertyAttributeReader*, const char*)
{}
void ColorCurveHandler::WriteGUIValuesIntoProperty([[maybe_unused]] size_t index, [[maybe_unused]] CColorGradientCtrl* GUI, [[maybe_unused]] property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node)
{}
bool ColorCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CColorGradientCtrl* GUI, const property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node)
{
GUI->SetSpline(reinterpret_cast<ISplineInterpolator*>(instance.m_spline));
return false;
}
@@ -16,7 +16,6 @@
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>
#include "ReflectedVar.h"
#include "Util/VariablePropertyType.h"
#include "Controls/ColorGradientCtrl.h"
#include "Controls/SplineCtrl.h"
#include <QWidget>
#endif
@@ -82,17 +81,4 @@ public:
void OnSplineChange(CSplineCtrl*);
};
class ColorCurveHandler : public QObject, public AzToolsFramework::PropertyHandler < CReflectedVarSpline, CColorGradientCtrl>
{
public:
AZ_CLASS_ALLOCATOR(ColorCurveHandler, AZ::SystemAllocator, 0);
bool IsDefaultHandler() const override { return false; }
QWidget* CreateGUI(QWidget *pParent) override;
AZ::u32 GetHandlerName(void) const override { return AZ_CRC("ePropertyColorCurve", 0xa30da4ec); }
void ConsumeAttribute(CColorGradientCtrl* GUI, AZ::u32 attrib, AzToolsFramework::PropertyAttributeReader* attrValue, const char* debugName) override;
void WriteGUIValuesIntoProperty(size_t index, CColorGradientCtrl* GUI, property_t& instance, AzToolsFramework::InstanceDataNode* node) override;
bool ReadValuesIntoGUI(size_t index, CColorGradientCtrl* GUI, const property_t& instance, AzToolsFramework::InstanceDataNode* node) override;
};
#endif // CRYINCLUDE_EDITOR_UTILS_PROPERTYMISCCTRL_H
@@ -76,6 +76,7 @@ namespace AZ
else if (info.m_status == AzToolsFramework::SourceControlStatus::SCS_ProviderIsDown)
{
message = "Failed to put entries/dependencies into source control as the provider is not available.\n";
reportAsWarning = true;
}
else if (info.m_status == AzToolsFramework::SourceControlStatus::SCS_CertificateInvalid)
{
-2
View File
@@ -330,8 +330,6 @@ set(FILES
Commands/CommandManager.h
Controls/BitmapToolTip.cpp
Controls/BitmapToolTip.h
Controls/ColorGradientCtrl.cpp
Controls/ColorGradientCtrl.h
Controls/ConsoleSCB.cpp
Controls/ConsoleSCB.h
Controls/ConsoleSCB.ui
@@ -0,0 +1,254 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
namespace AZ
{
JsonSerializationResult::ResultCode JsonImportResolver::ResolveNestedImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element)
{
using namespace JsonSerializationResult;
for (auto& path : importPathStack)
{
if (importPath == path)
{
return settings.m_reporting(
AZStd::string::format("'%s' was already imported in this chain. This indicates a cyclic dependency.", importPath.c_str()),
ResultCode(Tasks::Import, Outcomes::Catastrophic), element);
}
}
importPathStack.push_back(importPath);
AZ::StackedString importElement(AZ::StackedString::Format::JsonPointer);
JsonImportSettings nestedImportSettings;
nestedImportSettings.m_importer = settings.m_importer;
nestedImportSettings.m_reporting = settings.m_reporting;
nestedImportSettings.m_resolveFlags = ImportTracking::Dependencies;
ResultCode result = ResolveImports(jsonDoc, allocator, importPathStack, nestedImportSettings, importElement);
importPathStack.pop_back();
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode JsonImportResolver::ResolveImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, StackedString& element)
{
using namespace JsonSerializationResult;
if (jsonDoc.IsObject())
{
for (auto& field : jsonDoc.GetObject())
{
if(strncmp(field.name.GetString(), JsonSerialization::ImportDirectiveIdentifier, field.name.GetStringLength()) == 0)
{
const rapidjson::Value& importDirective = field.value;
AZ::IO::FixedMaxPath importAbsPath = importPathStack.back();
importAbsPath.RemoveFilename();
AZStd::string importName;
if (importDirective.IsObject())
{
auto filenameField = importDirective.FindMember("filename");
if (filenameField != importDirective.MemberEnd())
{
importName = AZStd::string(filenameField->value.GetString(), filenameField->value.GetStringLength());
}
}
else
{
importName = AZStd::string(importDirective.GetString(), importDirective.GetStringLength());
}
importAbsPath.Append(importName);
rapidjson::Value patch;
ResultCode resolveResult = settings.m_importer->ResolveImport(&jsonDoc, patch, importDirective, importAbsPath, allocator);
if (resolveResult.GetOutcome() == Outcomes::Catastrophic)
{
return resolveResult;
}
if ((settings.m_resolveFlags & ImportTracking::Imports) == ImportTracking::Imports)
{
rapidjson::Pointer path(element.Get().data(), element.Get().size());
settings.m_importer->AddImportDirective(path, importName);
}
if ((settings.m_resolveFlags & ImportTracking::Dependencies) == ImportTracking::Dependencies)
{
settings.m_importer->AddImportedFile(importAbsPath.String());
}
ResultCode result = ResolveNestedImports(jsonDoc, allocator, importPathStack, settings, importAbsPath, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
settings.m_importer->ApplyPatch(jsonDoc, patch, allocator);
}
else if (field.value.IsObject() || field.value.IsArray())
{
ScopedStackedString entryName(element, AZStd::string_view(field.name.GetString(), field.name.GetStringLength()));
ResultCode result = ResolveImports(field.value, allocator, importPathStack, settings, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
}
}
}
else if(jsonDoc.IsArray())
{
int index = 0;
for (rapidjson::Value::ValueIterator elem = jsonDoc.Begin(); elem != jsonDoc.End(); ++elem, ++index)
{
if (!elem->IsObject() && !elem->IsArray())
{
continue;
}
ScopedStackedString entryName(element, index);
ResultCode result = ResolveImports(*elem, allocator, importPathStack, settings, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
}
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode JsonImportResolver::RestoreImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (jsonDoc.IsObject() || jsonDoc.IsArray())
{
const BaseJsonImporter::ImportDirectivesList& importDirectives = settings.m_importer->GetImportDirectives();
for (auto& import : importDirectives)
{
rapidjson::Pointer importPtr = import.first;
rapidjson::Value* currentValue = importPtr.Get(jsonDoc);
rapidjson::Value importedValue(rapidjson::kObjectType);
importedValue.AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(import.second.c_str()), allocator);
ResultCode resolveResult = JsonSerialization::ResolveImports(importedValue, allocator, settings);
if (resolveResult.GetOutcome() == Outcomes::Catastrophic)
{
return resolveResult;
}
rapidjson::Value patch;
settings.m_importer->CreatePatch(patch, importedValue, *currentValue, allocator);
settings.m_importer->RestoreImport(currentValue, patch, allocator, import.second);
}
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator)
{
using namespace JsonSerializationResult;
auto importedObject = JsonSerializationUtils::ReadJsonFile(importedFilePath.Native());
if (importedObject.IsSuccess())
{
rapidjson::Value& importedDoc = importedObject.GetValue();
if (importDirective.IsObject())
{
auto patchField = importDirective.FindMember("patch");
if (patchField != importDirective.MemberEnd())
{
patch.CopyFrom(patchField->value, allocator);
}
}
importPtr->CopyFrom(importedDoc, allocator);
}
else
{
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::RestoreImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, const AZStd::string& importFilename)
{
using namespace JsonSerializationResult;
importPtr->SetObject();
if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty()))
{
rapidjson::Value importDirective(rapidjson::kObjectType);
importDirective.AddMember(rapidjson::StringRef("filename"), rapidjson::StringRef(importFilename.c_str()), allocator);
importDirective.AddMember(rapidjson::StringRef("patch"), patch, allocator);
importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), importDirective, allocator);
}
else
{
importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(importFilename.c_str()), allocator);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::ApplyPatch(rapidjson::Value& target,
const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator)
{
using namespace JsonSerializationResult;
if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty()))
{
return AZ::JsonSerialization::ApplyPatch(target, allocator, patch, JsonMergeApproach::JsonMergePatch);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::CreatePatch(rapidjson::Value& patch,
const rapidjson::Value& source, const rapidjson::Value& target,
rapidjson::Document::AllocatorType& allocator)
{
return JsonSerialization::CreatePatch(patch, allocator, source, target, JsonMergeApproach::JsonMergePatch);
}
void BaseJsonImporter::AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile)
{
m_importDirectives.emplace_back(jsonPtr, AZStd::move(importFile));
}
void BaseJsonImporter::AddImportedFile(AZStd::string importedFile)
{
m_importedFiles.insert(AZStd::move(importedFile));
}
const BaseJsonImporter::ImportDirectivesList& BaseJsonImporter::GetImportDirectives()
{
return m_importDirectives;
}
const BaseJsonImporter::ImportedFilesList& BaseJsonImporter::GetImportedFiles()
{
return m_importedFiles;
}
} // namespace AZ
@@ -0,0 +1,108 @@
/*
* 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/IO/Path/Path.h>
#include <AzCore/JSON/document.h>
#include <AzCore/JSON/pointer.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/Serialization/Json/JsonSerializationResult.h>
#include <AzCore/Serialization/Json/StackedString.h>
namespace AZ
{
struct JsonImportSettings;
class BaseJsonImporter
{
public:
AZ_RTTI(BaseJsonImporter, "{7B225807-7B43-430F-8B11-C794DCF5ACA5}");
using ImportDirectivesList = AZStd::vector<AZStd::pair<rapidjson::Pointer, AZStd::string>>;
using ImportedFilesList = AZStd::unordered_set<AZStd::string>;
virtual JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator);
virtual JsonSerializationResult::ResultCode RestoreImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator,
const AZStd::string& importFilename);
virtual JsonSerializationResult::ResultCode ApplyPatch(rapidjson::Value& target,
const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator);
virtual JsonSerializationResult::ResultCode CreatePatch(rapidjson::Value& patch,
const rapidjson::Value& source, const rapidjson::Value& target,
rapidjson::Document::AllocatorType& allocator);
void AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile);
const ImportDirectivesList& GetImportDirectives();
void AddImportedFile(AZStd::string importedFile);
const ImportedFilesList& GetImportedFiles();
virtual ~BaseJsonImporter() = default;
protected:
ImportDirectivesList m_importDirectives;
ImportedFilesList m_importedFiles;
};
enum class ImportTracking : AZ::u8
{
None = 0,
Dependencies = (1<<0),
Imports = (1<<1),
All = (Dependencies | Imports)
};
AZ_DEFINE_ENUM_BITWISE_OPERATORS(ImportTracking);
class JsonImportResolver final
{
public:
using ImportPathStack = AZStd::vector<AZ::IO::FixedMaxPath>;
JsonImportResolver() = delete;
JsonImportResolver& operator=(const JsonImportResolver& rhs) = delete;
JsonImportResolver& operator=(JsonImportResolver&& rhs) = delete;
JsonImportResolver(const JsonImportResolver& rhs) = delete;
JsonImportResolver(JsonImportResolver&& rhs) = delete;
~JsonImportResolver() = delete;
static JsonSerializationResult::ResultCode ResolveImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, StackedString& element);
static JsonSerializationResult::ResultCode RestoreImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
private:
static JsonSerializationResult::ResultCode ResolveNestedImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element);
};
struct JsonImportSettings final
{
JsonSerializationResult::JsonIssueCallback m_reporting;
BaseJsonImporter* m_importer = nullptr;
ImportTracking m_resolveFlags = ImportTracking::All;
AZ::IO::FixedMaxPath m_loadedJsonPath;
};
} // namespace AZ
@@ -706,7 +706,7 @@ namespace AZ
rapidjson::Value(rapidjson::kNullType), field.value, element, settings);
}
if (result.GetOutcome() == Outcomes::Success)
if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults)
{
rapidjson::Value name;
name.CopyFrom(field.name, allocator, true);
@@ -717,6 +717,10 @@ namespace AZ
{
return result;
}
else
{
resultCode.Combine(result);
}
}
// Do an extra pass to find all the fields that are removed.
@@ -751,7 +755,7 @@ namespace AZ
rapidjson::Value value;
ResultCode result = CreateMergePatchInternal(value, allocator,
rapidjson::Value(rapidjson::kNullType), field.value, element, settings);
if (result.GetOutcome() == Outcomes::Success)
if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults)
{
rapidjson::Value name;
name.CopyFrom(field.name, allocator, true);
@@ -762,11 +766,20 @@ namespace AZ
{
return result;
}
else
{
resultCode.Combine(result);
}
}
if (target.MemberCount() == 0)
{
resultCode.Combine(settings.m_reporting("Added empty object to JSON Merge Patch.",
ResultCode(Tasks::CreatePatch, Outcomes::Success), element));
}
}
patch = AZStd::move(resultValue);
resultCode.Combine(ResultCode(Tasks::CreatePatch, Outcomes::Success));
return resultCode;
}
else
@@ -10,6 +10,7 @@
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/Json/BaseJsonSerializer.h>
#include <AzCore/Serialization/Json/JsonDeserializer.h>
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonMerger.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/JsonSerializer.h>
@@ -19,11 +20,6 @@
namespace AZ
{
const char* JsonSerialization::TypeIdFieldIdentifier = "$type";
const char* JsonSerialization::DefaultStringIdentifier = "{}";
const char* JsonSerialization::KeyFieldIdentifier = "Key";
const char* JsonSerialization::ValueFieldIdentifier = "Value";
namespace JsonSerializationInternal
{
template<typename T>
@@ -394,6 +390,60 @@ namespace AZ
}
}
JsonSerializationResult::ResultCode JsonSerialization::ResolveImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (settings.m_importer == nullptr)
{
AZ_Assert(false, "Importer object needs to be provided");
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode
{
return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target);
};
if (!settings.m_reporting)
{
settings.m_reporting = issueReportingCallback;
}
JsonImportResolver::ImportPathStack importPathStack;
importPathStack.push_back(settings.m_loadedJsonPath);
StackedString element(StackedString::Format::JsonPointer);
return JsonImportResolver::ResolveImports(jsonDoc, allocator, importPathStack, settings, element);
}
JsonSerializationResult::ResultCode JsonSerialization::RestoreImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (settings.m_importer == nullptr)
{
AZ_Assert(false, "Importer object needs to be provided");
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode
{
return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target);
};
if (!settings.m_reporting)
{
settings.m_reporting = issueReportingCallback;
}
settings.m_resolveFlags = ImportTracking::None;
return JsonImportResolver::RestoreImports(jsonDoc, allocator, settings);
}
JsonSerializationResult::ResultCode JsonSerialization::DefaultIssueReporter(AZStd::string& scratchBuffer,
AZStd::string_view message, JsonSerializationResult::ResultCode result, AZStd::string_view path)
{
@@ -18,6 +18,8 @@
namespace AZ
{
class BaseJsonSerializer;
struct JsonImportSettings;
enum class JsonMergeApproach
{
@@ -51,10 +53,11 @@ namespace AZ
class JsonSerialization final
{
public:
static const char* TypeIdFieldIdentifier;
static const char* DefaultStringIdentifier;
static const char* KeyFieldIdentifier;
static const char* ValueFieldIdentifier;
static constexpr const char* TypeIdFieldIdentifier = "$type";
static constexpr const char* DefaultStringIdentifier = "{}";
static constexpr const char* KeyFieldIdentifier = "Key";
static constexpr const char* ValueFieldIdentifier = "Value";
static constexpr const char* ImportDirectiveIdentifier = "$import";
//! Merges two json values together by applying "patch" to "target" using the selected merge algorithm.
//! This version of ApplyPatch is destructive to "target". If the patch can't be correctly applied it will
@@ -284,6 +287,22 @@ namespace AZ
//! @return An enum containing less, equal or greater. In case of an error, the value for the enum will "error".
static JsonSerializerCompareResult Compare(const rapidjson::Value& lhs, const rapidjson::Value& rhs);
//! Resolves all import directives, including nested imports, in the given document. An importer object needs to be passed
//! in through the settings.
//! @param jsonDoc The json document in which to resolve imports.
//! @param allocator The allocator associated with the json document.
//! @param settings Additional settings that control the way the imports are resolved.
static JsonSerializationResult::ResultCode ResolveImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
//! Restores all import directives that were present in the json document. The same importer object that was
//! passed into ResolveImports through the settings needs to be passed here through settings as well.
//! @param jsonDoc The json document in which to restore imports.
//! @param allocator The allocator associated with the json document.
//! @param settings Additional settings that control the way the imports are restored.
static JsonSerializationResult::ResultCode RestoreImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
private:
JsonSerialization() = delete;
~JsonSerialization() = delete;
@@ -69,6 +69,9 @@ namespace AZ
case Tasks::CreatePatch:
target.append("a create patch operation ");
break;
case Tasks::Import:
target.append("an import operation");
break;
default:
target.append("an unknown operation ");
break;
@@ -32,7 +32,8 @@ namespace AZ
ReadField, //!< Task to read a field from JSON to a value.
WriteValue, //!< Task to write a value to a JSON field.
Merge, //!< Task to merge two JSON values/documents together.
CreatePatch //!< Task to create a patch to transform one value/document to another.
CreatePatch, //!< Task to create a patch to transform one value/document to another.
Import //!< Task to import a JSON document.
};
//! Describes how the task was processed.
@@ -522,6 +522,8 @@ set(FILES
Serialization/Json/IntSerializer.cpp
Serialization/Json/JsonDeserializer.h
Serialization/Json/JsonDeserializer.cpp
Serialization/Json/JsonImporter.cpp
Serialization/Json/JsonImporter.h
Serialization/Json/JsonMerger.h
Serialization/Json/JsonMerger.cpp
Serialization/Json/JsonSerialization.h
@@ -0,0 +1,413 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <Tests/Serialization/Json/JsonSerializationTests.h>
namespace JsonSerializationTests
{
class JsonImportingTests;
class JsonImporterCustom
: public AZ::BaseJsonImporter
{
public:
AZ_RTTI(JsonImporterCustom, "{003F5896-71E0-4A50-A14F-08C319B06AD0}");
AZ::JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) override;
JsonImporterCustom(JsonImportingTests* tests)
{
testClass = tests;
}
private:
JsonImportingTests* testClass;
};
class JsonImportingTests
: public BaseJsonSerializerFixture
{
public:
void SetUp() override
{
BaseJsonSerializerFixture::SetUp();
}
void TearDown() override
{
BaseJsonSerializerFixture::TearDown();
}
void GetTestDocument(const AZStd::string& docName, rapidjson::Document& out)
{
const char *objectJson = R"({
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
})";
const char *arrayJson = R"([
{ "element_1" : "value_1" },
{ "element_2" : "value_2" },
{ "element_3" : "value_3" }
])";
const char *nestedImportJson = R"({
"desc" : "Nested Import",
"obj" : {"$import" : "object.json"}
})";
const char *nestedImportCycle1Json = R"({
"desc" : "Nested Import Cycle 1",
"obj" : {"$import" : "nested_import_c2.json"}
})";
const char *nestedImportCycle2Json = R"({
"desc" : "Nested Import Cycle 2",
"obj" : {"$import" : "nested_import_c1.json"}
})";
if (docName.compare("object.json") == 0)
{
out.Parse(objectJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("array.json") == 0)
{
out.Parse(arrayJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import.json") == 0)
{
out.Parse(nestedImportJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import_c1.json") == 0)
{
out.Parse(nestedImportCycle1Json);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import_c2.json") == 0)
{
out.Parse(nestedImportCycle2Json);
ASSERT_FALSE(out.HasParseError());
}
}
protected:
void TestImportLoadStore(const char* input, const char* expectedImportedValue)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
rapidjson::Document expectedOutcome;
expectedOutcome.Parse(expectedImportedValue);
ASSERT_FALSE(expectedOutcome.HasParseError());
TestResolveImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutcome.GetObject());
rapidjson::Document originalInput;
originalInput.Parse(input);
ASSERT_FALSE(originalInput.HasParseError());
TestRestoreImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), originalInput.GetObject());
m_jsonDocument->SetObject();
delete importerObj;
}
void TestImportCycle(const char* input)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
AZ::JsonSerializationResult::ResultCode result = TestResolveImports(importerObj);
EXPECT_EQ(result.GetOutcome(), AZ::JsonSerializationResult::Outcomes::Catastrophic);
m_jsonDocument->SetObject();
delete importerObj;
}
void TestInsertNewImport(const char* input, const char* expectedRestoredValue)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
TestResolveImports(importerObj);
importerObj->AddImportDirective(rapidjson::Pointer("/object_2"), "object.json");
rapidjson::Document expectedOutput;
expectedOutput.Parse(expectedRestoredValue);
ASSERT_FALSE(expectedOutput.HasParseError());
TestRestoreImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutput.GetObject());
m_jsonDocument->SetObject();
delete importerObj;
}
AZ::JsonSerializationResult::ResultCode TestResolveImports(JsonImporterCustom* importerObj)
{
AZ::JsonImportSettings settings;
settings.m_importer = importerObj;
return AZ::JsonSerialization::ResolveImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings);
}
AZ::JsonSerializationResult::ResultCode TestRestoreImports(JsonImporterCustom* importerObj)
{
AZ::JsonImportSettings settings;
settings.m_importer = importerObj;
return AZ::JsonSerialization::RestoreImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings);
}
};
AZ::JsonSerializationResult::ResultCode JsonImporterCustom::ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective, const AZ::IO::FixedMaxPath& importedFilePath,
rapidjson::Document::AllocatorType& allocator)
{
AZ::JsonSerializationResult::ResultCode resultCode(AZ::JsonSerializationResult::Tasks::Import);
rapidjson::Document importedDoc;
testClass->GetTestDocument(importedFilePath.String(), importedDoc);
if (importDirective.IsObject())
{
auto patchField = importDirective.FindMember("patch");
if (patchField != importDirective.MemberEnd())
{
patch.CopyFrom(patchField->value, allocator);
}
}
importPtr->CopyFrom(importedDoc, allocator);
return resultCode;
}
// Test Cases
TEST_F(JsonImportingTests, ImportSimpleObjectTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object": {"$import" : "object.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object": {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleObjectPatchTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object": {
"$import" : {
"filename" : "object.json",
"patch" : { "field_2" : "patched_value" }
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object": {
"field_1" : "value_1",
"field_2" : "patched_value",
"field_3" : "value_3"
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleArrayTest)
{
const char* inputFile = R"(
{
"name" : "simple_array_import",
"object": {"$import" : "array.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_array_import",
"object": [
{ "element_1" : "value_1" },
{ "element_2" : "value_2" },
{ "element_3" : "value_3" }
]
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleArrayPatchTest)
{
const char* inputFile = R"(
{
"name" : "simple_array_import",
"object": {
"$import" : {
"filename" : "array.json",
"patch" : [ { "element_1" : "patched_value" } ]
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_array_import",
"object": [
{ "element_1" : "patched_value" }
]
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportTest)
{
const char* inputFile = R"(
{
"name" : "nested_import",
"object": {"$import" : "nested_import.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "nested_import",
"object": {
"desc" : "Nested Import",
"obj" : {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportPatchTest)
{
const char* inputFile = R"(
{
"name" : "nested_import",
"object": {
"$import" : {
"filename" : "nested_import.json",
"patch" : { "obj" : { "field_3" : "patched_value" } }
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "nested_import",
"object": {
"desc" : "Nested Import",
"obj" : {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "patched_value"
}
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportCycleTest)
{
const char* inputFile = R"(
{
"name" : "nested_import_cycle",
"object": {"$import" : "nested_import_c1.json"}
}
)";
TestImportCycle(inputFile);
}
TEST_F(JsonImportingTests, InsertNewImportTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object_1": {"$import" : "object.json"},
"object_2": {
"field_1" : "other_value",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object_1": {"$import" : "object.json"},
"object_2": {
"$import" : {
"filename" : "object.json",
"patch" : { "field_1" : "other_value" }
}
}
}
)";
TestInsertNewImport(inputFile, expectedOutput);
}
}
@@ -121,6 +121,7 @@ set(FILES
Serialization/Json/TestCases_Classes.cpp
Serialization/Json/TestCases_Compare.cpp
Serialization/Json/TestCases_Enum.cpp
Serialization/Json/TestCases_Importing.cpp
Serialization/Json/TestCases_Patching.cpp
Serialization/Json/TestCases_Pointers.h
Serialization/Json/TestCases_Pointers.cpp
@@ -43,7 +43,7 @@ namespace AzFramework
class IMatchmakingAsyncRequests
{
public:
AZ_RTTI(ISessionAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}");
AZ_RTTI(IMatchmakingAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}");
IMatchmakingAsyncRequests() = default;
virtual ~IMatchmakingAsyncRequests() = default;
@@ -60,4 +60,31 @@ namespace AzFramework
// @param stopMatchmakingRequest The request of StopMatchmaking operation
virtual void StopMatchmakingAsync(const StopMatchmakingRequest& stopMatchmakingRequest) = 0;
};
//! MatchmakingAsyncRequestNotifications
//! The notifications correspond to matchmaking async requests
class MatchmakingAsyncRequestNotifications
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes
virtual void OnAcceptMatchAsyncComplete() = 0;
// OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes
// @param matchmakingTicketId The unique identifier for the matchmaking ticket
virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0;
// OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes
virtual void OnStopMatchmakingAsyncComplete() = 0;
};
using MatchmakingAsyncRequestNotificationBus = AZ::EBus<MatchmakingAsyncRequestNotifications>;
} // namespace AzFramework
@@ -13,36 +13,10 @@
namespace AzFramework
{
//! MatchmakingAsyncRequestNotifications
//! The notifications correspond to matchmaking async requests
class MatchmakingAsyncRequestNotifications
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes
virtual void OnAcceptMatchAsyncComplete() = 0;
// OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes
// @param matchmakingTicketId The unique identifier for the matchmaking ticket
virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0;
// OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes
virtual void OnStopMatchmakingAsyncComplete() = 0;
};
using MatchmakingAsyncRequestNotificationBus = AZ::EBus<MatchmakingAsyncRequestNotifications>;
//! MatchmakingNotifications
//! The matchmaking notifications to listen for performing required operations
class MatchAcceptanceNotifications
//! based on matchmaking ticket event
class MatchmakingNotifications
: public AZ::EBusTraits
{
public:
@@ -55,8 +29,18 @@ namespace AzFramework
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnMatchAcceptance is fired when DescribeMatchmaking ticket status is REQUIRES_ACCEPTANCE
// OnMatchAcceptance is fired when match is found and pending on acceptance
// Use this notification to accept found match
virtual void OnMatchAcceptance() = 0;
// OnMatchComplete is fired when match is complete
virtual void OnMatchComplete() = 0;
// OnMatchError is fired when match is processed with error
virtual void OnMatchError() = 0;
// OnMatchFailure is fired when match is failed to complete
virtual void OnMatchFailure() = 0;
};
using MatchAcceptanceNotificationBus = AZ::EBus<MatchAcceptanceNotifications>;
using MatchmakingNotificationBus = AZ::EBus<MatchmakingNotifications>;
} // namespace AzFramework
@@ -22,7 +22,7 @@ namespace AzFramework
AZStd::scoped_ptr<ProcessWatcher> pWatcher(LaunchProcess(processLaunchInfo, communicationType));
if (!pWatcher)
{
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
return false;
}
else
@@ -31,7 +31,7 @@ namespace AzFramework
ProcessCommunicator* pCommunicator = pWatcher->GetCommunicator();
if (!pCommunicator || !pCommunicator->IsValid())
{
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
return false;
}
else
@@ -308,7 +308,7 @@ namespace AzFramework
event.data.data32[2] = 0;
event.data.data32[3] = 1;
event.data.data32[4] = 0;
xcb_void_cookie_t xcbCheckResult = xcb_send_event(
[[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event(
m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
(const char*)&event);
AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set _NET_WM_STATE_FULLSCREEN");
@@ -333,7 +333,7 @@ namespace AzFramework
event.data.data32[2] = 0;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
xcb_void_cookie_t xcbCheckResult = xcb_send_event(
[[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event(
m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
(const char*)&event);
AZ_Assert(
@@ -7,17 +7,35 @@
*/
#include <AzFramework/Application/Application.h>
#include <sys/resource.h>
#if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
#include <AzFramework/XcbApplication.h>
#endif
constexpr rlim_t g_minimumOpenFileHandles = 65536L;
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
Application::Implementation* Application::Implementation::Create()
{
// The default open file limit for processes may not be enough for O3DE applications.
// We will need to increase to the recommended value if the current open file limit
// is not sufficient.
rlimit currentLimit;
int get_limit_result = getrlimit(RLIMIT_NOFILE, &currentLimit);
AZ_Warning("Application", get_limit_result == 0, "Unable to read current ulimit open file limits");
if ((get_limit_result == 0) && (currentLimit.rlim_cur < g_minimumOpenFileHandles || currentLimit.rlim_max < g_minimumOpenFileHandles))
{
rlimit newLimit;
newLimit.rlim_cur = g_minimumOpenFileHandles; // Soft Limit
newLimit.rlim_max = g_minimumOpenFileHandles; // Hard Limit
[[maybe_unused]] int set_limit_result = setrlimit(RLIMIT_NOFILE, &newLimit);
AZ_Assert(set_limit_result == 0, "Unable to update open file limits");
}
#if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
return aznew XcbApplication();
#elif PAL_TRAIT_LINUX_WINDOW_MANAGER_WAYLAND
@@ -91,31 +91,42 @@ namespace AzFramework
return processId == 0;
}
/*! Executes a command in the child process after the fork operation has been executed.
* This function will never return. If the execvp command fails this will call _exit with
* the errno value as the return value since continuing execution after a execvp command
* is invalid (it will be running the parent's code and in its address space and will
* cause many issues).
/*! Executes a command in the child process after the fork operation
* has been executed. This function will never return. If the execvpe
* command fails this will call _exit since continuing execution after
* a execvpe command is invalid (it will be running the parent's code
* and in its address space and will cause many issues).
*
* This function runs after a `fork()` call. `fork()` creates a copy of
* the current process, including the current state of the process's
* memory, at the time the call is made. However, it only creates a
* copy of the one thread that called `fork()`. This means that if any
* mutexes are locked by other threads at the time of `fork()`, those
* mutexes will remain locked in the child process, with no way to
* unlock them. So this function needs to ensure that it does as little
* work as possible.
*
* \param commandAndArgs - Array of strings that has the command to execute in index 0 with any args for the command following. Last element must be a null pointer.
* \param envionrmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer.
* \param environmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer.
* \param processLaunchInfo - struct containing information about luanching the command
* \param startupInfo - struct containing information needed to startup the command
* \param errorPipe - a pipe file descriptor used to communicate a failed execvpe call's error code to the parent process
*/
void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo)
[[noreturn]] static void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo, const AZStd::array<int, 2>& errorPipe)
{
close(errorPipe[0]);
if (!processLaunchInfo.m_workingDirectory.empty())
{
int res = chdir(processLaunchInfo.m_workingDirectory.c_str());
if (res != 0)
{
std::cerr << strerror(errno) << std::endl;
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to change the launched process' directory to '%s'.", processLaunchInfo.m_workingDirectory.c_str());
write(errorPipe[1], &errno, sizeof(int));
// We *have* to _exit as we are the child process and simply
// returning at this point would mean we would start running
// the code from our parent process and that will just wreck
// havoc.
_exit(errno);
_exit(0);
}
}
@@ -135,15 +146,17 @@ namespace AzFramework
startupInfo.SetupHandlesForChildProcess();
execve(commandAndArgs[0], commandAndArgs, environmentVariables);
execvpe(commandAndArgs[0], commandAndArgs, environmentVariables);
const int errval = errno;
// If we get here then execve failed to run the requested program and
// If we get here then execvpe failed to run the requested program and
// we have an error. In this case we need to exit the child process
// to stop it from continuing to run as a clone of the parent
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process %s : errno = %s ", commandAndArgs[0], strerror(errno));
std::cerr << strerror(errno) << std::endl;
// to stop it from continuing to run as a clone of the parent.
// Communicate the error code back to the parent via a pipe for the
// parent to read.
write(errorPipe[1], &errval, sizeof(errval));
_exit(errno);
_exit(0);
}
}
@@ -212,9 +225,8 @@ namespace AzFramework
AZStd::string outputString;
bool inQuotes = false;
for (size_t pos = 0; pos < processLaunchInfo.m_commandlineParameters.size(); ++pos)
for (const char currentChar : processLaunchInfo.m_commandlineParameters)
{
char currentChar = processLaunchInfo.m_commandlineParameters[pos];
if (currentChar == '"')
{
inQuotes = !inQuotes;
@@ -231,7 +243,7 @@ namespace AzFramework
outputString.push_back(currentChar);
}
}
if (!outputString.empty())
{
commandTokens.push_back(outputString);
@@ -249,10 +261,10 @@ namespace AzFramework
return false;
}
// Because of the way execve is defined we need to copy the strings from
// Because of the way execvpe is defined we need to copy the strings from
// AZ::string (using c_str() returns a const char*) into a non-const char*
// Need to add one more as exec requires the array's last element to be a null pointer
// Need to add one more as execvpe requires the array's last element to be a null pointer
char** commandAndArgs = new char*[commandTokens.size() + 1];
for (int i = 0; i < commandTokens.size(); ++i)
{
@@ -275,7 +287,7 @@ namespace AzFramework
azstrcat(environmentVariable.get(), envVarString.size() + 1, envVarString.c_str());
environmentVariablesVector.emplace_back(environmentVariable.get());
}
// Adding one more as exec expects the array to have a nullptr as the last element
// Adding one more as execvpe expects the array to have a nullptr as the last element
environmentVariablesVector.emplace_back(nullptr);
environmentVariables = environmentVariablesVector.data();
}
@@ -288,15 +300,50 @@ namespace AzFramework
AZ_Assert(environmentVariables, "Environment variables for current process not available\n");
}
// Set up a pipe to communicate the error code from the subprocess's execvpe call
AZStd::array<int, 2> childErrorPipeFds{};
pipe(childErrorPipeFds.data());
// This configures the write end of the pipe to close on calls to `exec`
fcntl(childErrorPipeFds[1], F_SETFD, fcntl(childErrorPipeFds[1], F_GETFD) | FD_CLOEXEC);
pid_t child_pid = fork();
if (IsIdChildProcess(child_pid))
{
ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo);
ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo, childErrorPipeFds);
}
processData.m_childProcessId = child_pid;
// Close these handles as they are only to be used by the child process
processData.m_startupInfo.CloseAllHandles();
close(childErrorPipeFds[1]);
{
int errorCodeFromChild = 0;
int count = 0;
// Read from the error pipe.
// * If the child's call to execvpe succeeded, then the pipe will
// be closed due to setting FD_CLOEXEC on the write end of the
// pipe. `read()` will return 0.
// * If the child's call to execvpe failed, the child will have
// written the error code to the pipe. `read()` will return >0, and
// the data to be read is the error code from execvpe.
while ((count = read(childErrorPipeFds[0], &errorCodeFromChild, sizeof(errorCodeFromChild))) == -1)
{
if (errno != EAGAIN && errno != EINTR)
{
break;
}
}
if (count)
{
AZ_TracePrintf("Process Watcher", "ProcessLauncher::LaunchProcess: Unable to launch process %s : errno = %s\n", commandAndArgs[0], strerror(errorCodeFromChild));
processData.m_childProcessIsDone = true;
child_pid = -1;
}
}
close(childErrorPipeFds[0]);
processData.m_childProcessId = child_pid;
for (int i = 0; i < commandTokens.size(); i++)
{
@@ -7,6 +7,7 @@
*/
#include <dlfcn.h>
#include <iostream>
#include <AzCore/IO/Path/Path.h>
#include <AzTest/Platform.h>
#include <sys/types.h>
@@ -20,11 +21,16 @@ public:
explicit ModuleHandle(const std::string& lib)
: m_libHandle(nullptr)
{
std::string libext = lib;
if (!AZ::Test::EndsWith(libext, ".dylib"))
AZ::IO::FixedMaxPath libext = AZStd::string_view{ lib.c_str(), lib.size() };
if (!libext.Stem().Native().starts_with(AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX))
{
libext += ".dylib";
libext = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX + libext.Native();
}
if (libext.Extension() != AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION)
{
libext.Native() += AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION;
}
m_libHandle = dlopen(libext.c_str(), RTLD_NOW);
const char* error = dlerror();
if (error)
@@ -53,7 +53,7 @@ namespace AzToolsFramework
AZ_Assert(m_loaderInterface != nullptr,
"Couldn't get prefab loader interface, it's a requirement for PrefabEntityOwnership system to work");
m_rootInstance = AZStd::unique_ptr<Prefab::Instance>(m_prefabSystemComponent->CreatePrefab({}, {}, "NewLevel.prefab"));
m_rootInstance = AZStd::unique_ptr<Prefab::Instance>(m_prefabSystemComponent->CreatePrefab({}, {}, "newLevel.prefab"));
m_sliceOwnershipService.BusConnect(m_entityContextId);
m_sliceOwnershipService.m_shouldAssertForLegacySlicesUsage = m_shouldAssertForLegacySlicesUsage;
m_editorSliceOwnershipService.BusConnect();
@@ -9,6 +9,8 @@
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Math/Uuid.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
@@ -38,6 +40,13 @@ namespace AzToolsFramework
AZ::Edit::SliceFlags::DontGatherReference);
}
}
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty(
"EditorPrefabComponentTypeId", BehaviorConstant(AZ::Uuid(EditorPrefabComponent::EditorPrefabComponentTypeId)))
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
}
}
void EditorPrefabComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
@@ -16,7 +16,9 @@ namespace AzToolsFramework
class EditorPrefabComponent : public AzToolsFramework::Components::EditorComponentBase
{
public:
AZ_COMPONENT(EditorPrefabComponent, "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}", EditorComponentBase);
static constexpr const char* const EditorPrefabComponentTypeId = "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}";
AZ_COMPONENT(EditorPrefabComponent, EditorPrefabComponentTypeId, EditorComponentBase);
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
@@ -28,7 +28,9 @@ namespace AzToolsFramework::Prefab
"Instance Entity Mapper Interface could not be found. "
"Check that it is being correctly initialized.");
EditorEntityInfoNotificationBus::Handler::BusConnect();
EditorEntityContextNotificationBus::Handler::BusConnect();
PrefabPublicNotificationBus::Handler::BusConnect();
AZ::Interface<PrefabFocusInterface>::Register(this);
AZ::Interface<PrefabFocusPublicInterface>::Register(this);
}
@@ -37,10 +39,12 @@ namespace AzToolsFramework::Prefab
{
AZ::Interface<PrefabFocusPublicInterface>::Unregister(this);
AZ::Interface<PrefabFocusInterface>::Unregister(this);
PrefabPublicNotificationBus::Handler::BusDisconnect();
EditorEntityContextNotificationBus::Handler::BusDisconnect();
EditorEntityInfoNotificationBus::Handler::BusDisconnect();
}
void PrefabFocusHandler::Initialize()
void PrefabFocusHandler::InitializeEditorInterfaces()
{
m_containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get();
AZ_Assert(
@@ -55,13 +59,6 @@ namespace AzToolsFramework::Prefab
"Prefab - PrefabFocusHandler - "
"Focus Mode Interface could not be found. "
"Check that it is being correctly initialized.");
m_instanceEntityMapperInterface = AZ::Interface<InstanceEntityMapperInterface>::Get();
AZ_Assert(
m_instanceEntityMapperInterface,
"Prefab - PrefabFocusHandler - "
"Instance Entity Mapper Interface could not be found. "
"Check that it is being correctly initialized.");
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnOwningPrefab(AZ::EntityId entityId)
@@ -90,12 +87,12 @@ namespace AzToolsFramework::Prefab
PrefabFocusOperationResult PrefabFocusHandler::FocusOnPathIndex([[maybe_unused]] AzFramework::EntityContextId entityContextId, int index)
{
if (index < 0 || index >= m_instanceFocusVector.size())
if (index < 0 || index >= m_instanceFocusHierarchy.size())
{
return AZ::Failure(AZStd::string("Prefab Focus Handler: Invalid index on FocusOnPathIndex."));
}
InstanceOptionalReference focusedInstance = m_instanceFocusVector[index];
InstanceOptionalReference focusedInstance = m_instanceFocusHierarchy[index];
FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId());
@@ -134,42 +131,38 @@ namespace AzToolsFramework::Prefab
return AZ::Failure(AZStd::string("Prefab Focus Handler: invalid instance to focus on."));
}
if (!m_isInitialized)
// Close all container entities in the old path.
CloseInstanceContainers(m_instanceFocusHierarchy);
m_focusedInstance = focusedInstance;
m_focusedTemplateId = focusedInstance->get().GetTemplateId();
AZ::EntityId containerEntityId;
if (focusedInstance->get().GetParentInstance() != AZStd::nullopt)
{
Initialize();
containerEntityId = focusedInstance->get().GetContainerEntityId();
}
if (!m_focusedInstance.has_value() || &m_focusedInstance->get() != &focusedInstance->get())
else
{
// Close all container entities in the old path
CloseInstanceContainers(m_instanceFocusVector);
m_focusedInstance = focusedInstance;
m_focusedTemplateId = focusedInstance->get().GetTemplateId();
AZ::EntityId containerEntityId;
if (focusedInstance->get().GetParentInstance() != AZStd::nullopt)
{
containerEntityId = focusedInstance->get().GetContainerEntityId();
}
else
{
containerEntityId = AZ::EntityId();
}
containerEntityId = AZ::EntityId();
}
// Focus on the descendants of the container entity
// Focus on the descendants of the container entity in the Editor, if the interface is initialized.
if (m_focusModeInterface)
{
m_focusModeInterface->SetFocusRoot(containerEntityId);
// Refresh path variables
RefreshInstanceFocusList();
// Open all container entities in the new path
OpenInstanceContainers(m_instanceFocusVector);
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
// Refresh path variables.
RefreshInstanceFocusList();
RefreshInstanceFocusPath();
// Open all container entities in the new path.
OpenInstanceContainers(m_instanceFocusHierarchy);
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
return AZ::Success();
}
@@ -220,49 +213,80 @@ namespace AzToolsFramework::Prefab
const int PrefabFocusHandler::GetPrefabFocusPathLength([[maybe_unused]] AzFramework::EntityContextId entityContextId) const
{
return aznumeric_cast<int>(m_instanceFocusVector.size());
return aznumeric_cast<int>(m_instanceFocusHierarchy.size());
}
void PrefabFocusHandler::OnEntityStreamLoadSuccess()
void PrefabFocusHandler::OnContextReset()
{
if (!m_isInitialized)
{
Initialize();
}
// Clear the old focus vector
m_instanceFocusVector.clear();
m_instanceFocusHierarchy.clear();
// Focus on the root prefab (AZ::EntityId() will default to it)
FocusOnPrefabInstanceOwningEntityId(AZ::EntityId());
}
void PrefabFocusHandler::OnEntityInfoUpdatedName(AZ::EntityId entityId, [[maybe_unused]]const AZStd::string& name)
{
// Determine if the entityId is the container for any of the instances in the vector
auto result = AZStd::find_if(
m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(),
[entityId](const InstanceOptionalReference& instance)
{
return (instance->get().GetContainerEntityId() == entityId);
}
);
if (result != m_instanceFocusHierarchy.end())
{
// Refresh the path and notify changes.
RefreshInstanceFocusPath();
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
}
void PrefabFocusHandler::OnPrefabInstancePropagationEnd()
{
// Refresh the path and notify changes in case propagation updated any container names.
RefreshInstanceFocusPath();
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
void PrefabFocusHandler::RefreshInstanceFocusList()
{
m_instanceFocusVector.clear();
m_instanceFocusPath.clear();
m_instanceFocusHierarchy.clear();
AZStd::list<InstanceOptionalReference> instanceFocusList;
// Use a support list to easily push front while traversing the prefab hierarchy
InstanceOptionalReference currentInstance = m_focusedInstance;
while (currentInstance.has_value())
{
instanceFocusList.push_front(currentInstance);
m_instanceFocusHierarchy.emplace_back(currentInstance);
currentInstance = currentInstance->get().GetParentInstance();
}
// Populate internals using the support list
for (auto& instance : instanceFocusList)
// Invert the vector, since we need the top instance to be at index 0
AZStd::reverse(m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end());
}
void PrefabFocusHandler::RefreshInstanceFocusPath()
{
m_instanceFocusPath.clear();
for (const InstanceOptionalReference& instance : m_instanceFocusHierarchy)
{
m_instanceFocusPath.Append(instance->get().GetContainerEntity()->get().GetName());
m_instanceFocusVector.emplace_back(instance);
}
}
void PrefabFocusHandler::OpenInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const
{
// If this is called outside the Editor, this interface won't be initialized.
if (!m_containerEntityInterface)
{
return;
}
for (const InstanceOptionalReference& instance : instances)
{
if (instance.has_value())
@@ -274,6 +298,12 @@ namespace AzToolsFramework::Prefab
void PrefabFocusHandler::CloseInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const
{
// If this is called outside the Editor, this interface won't be initialized.
if (!m_containerEntityInterface)
{
return;
}
for (const InstanceOptionalReference& instance : instances)
{
if (instance.has_value())
@@ -11,9 +11,11 @@
#include <AzCore/Memory/SystemAllocator.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/FocusMode/FocusModeInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabPublicNotificationBus.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
namespace AzToolsFramework
@@ -30,7 +32,9 @@ namespace AzToolsFramework::Prefab
class PrefabFocusHandler final
: private PrefabFocusInterface
, private PrefabFocusPublicInterface
, private PrefabPublicNotificationBus::Handler
, private EditorEntityContextNotificationBus::Handler
, private EditorEntityInfoNotificationBus::Handler
{
public:
AZ_CLASS_ALLOCATOR(PrefabFocusHandler, AZ::SystemAllocator, 0);
@@ -38,9 +42,8 @@ namespace AzToolsFramework::Prefab
PrefabFocusHandler();
~PrefabFocusHandler();
void Initialize();
// PrefabFocusInterface overrides ...
void InitializeEditorInterfaces() override;
PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) override;
TemplateId GetFocusedPrefabTemplateId(AzFramework::EntityContextId entityContextId) const override;
InstanceOptionalReference GetFocusedPrefabInstance(AzFramework::EntityContextId entityContextId) const override;
@@ -54,25 +57,34 @@ namespace AzToolsFramework::Prefab
const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override;
// EditorEntityContextNotificationBus overrides ...
void OnEntityStreamLoadSuccess() override;
void OnContextReset() override;
// EditorEntityInfoNotificationBus overrides ...
void OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZStd::string& name) override;
// PrefabPublicNotifications overrides ...
void OnPrefabInstancePropagationEnd();
private:
PrefabFocusOperationResult FocusOnPrefabInstance(InstanceOptionalReference focusedInstance);
void RefreshInstanceFocusList();
void RefreshInstanceFocusPath();
void OpenInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const;
void CloseInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const;
//! The instance the editor is currently focusing on.
InstanceOptionalReference m_focusedInstance;
//! The templateId of the focused instance.
TemplateId m_focusedTemplateId;
AZStd::vector<InstanceOptionalReference> m_instanceFocusVector;
//! The list of instances going from the root (index 0) to the focused instance.
AZStd::vector<InstanceOptionalReference> m_instanceFocusHierarchy;
//! A path containing the names of the containers in the instance focus hierarchy, separated with a /.
AZ::IO::Path m_instanceFocusPath;
ContainerEntityInterface* m_containerEntityInterface = nullptr;
FocusModeInterface* m_focusModeInterface = nullptr;
InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr;
bool m_isInitialized = false;
};
} // namespace AzToolsFramework::Prefab
@@ -26,6 +26,11 @@ namespace AzToolsFramework::Prefab
public:
AZ_RTTI(PrefabFocusInterface, "{F3CFA37B-5FD8-436A-9C30-60EB54E350E1}");
//! Initializes the editor interfaces for Prefab Focus mode.
//! If this is not called on initialization, the Prefab Focus Mode functions will still work
//! but won't trigger the Editor APIs to visualize focus mode on the UI.
virtual void InitializeEditorInterfaces() = 0;
//! Set the focused prefab instance to the owning instance of the entityId provided.
//! @param entityId The entityId of the entity whose owning instance we want the prefab system to focus on.
virtual PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) = 0;
@@ -974,7 +974,7 @@ namespace AzToolsFramework
return DeleteFromInstance(entityIds, true);
}
PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
DuplicatePrefabResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
{
if (entityIds.empty())
{
@@ -1021,6 +1021,7 @@ namespace AzToolsFramework
ScopedUndoBatch undoBatch("Duplicate Entities");
EntityIdList duplicatedEntityAndInstanceIds;
{
AZ_PROFILE_SCOPE(AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities");
@@ -1033,7 +1034,7 @@ namespace AzToolsFramework
if (!retrieveEntitiesAndInstancesOutcome.IsSuccess())
{
return AZStd::move(retrieveEntitiesAndInstancesOutcome);
return AZ::Failure(retrieveEntitiesAndInstancesOutcome.TakeError());
}
// Take a snapshot of the instance DOM before we manipulate it
@@ -1044,8 +1045,6 @@ namespace AzToolsFramework
PrefabDom instanceDomAfter;
instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator());
EntityIdList duplicatedEntityAndInstanceIds;
// Duplicate any nested entities and instances as requested
AZStd::unordered_map<InstanceAlias, Instance*> newInstanceAliasToOldInstanceMap;
AZStd::unordered_map<EntityAlias, EntityAlias> duplicateEntityAliasMap;
@@ -1114,7 +1113,7 @@ namespace AzToolsFramework
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds);
}
return AZ::Success();
return AZ::Success(AZStd::move(duplicatedEntityAndInstanceIds));
}
PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants)
@@ -63,7 +63,7 @@ namespace AzToolsFramework
PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override;
@@ -26,6 +26,7 @@ namespace AzToolsFramework
{
typedef AZ::Outcome<AZ::EntityId, AZStd::string> CreatePrefabResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> InstantiatePrefabResult;
typedef AZ::Outcome<EntityIdList, AZStd::string> DuplicatePrefabResult;
typedef AZ::Outcome<void, AZStd::string> PrefabOperationResult;
typedef AZ::Outcome<bool, AZStd::string> PrefabRequestResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> PrefabEntityResult;
@@ -160,14 +161,15 @@ namespace AzToolsFramework
/**
* Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance.
* @param entities The entities to duplicate.
* @return An outcome object; on failure, it comes with an error message detailing the cause of the error.
* @return An outcome object with a list of ids of target entities' duplicates if duplication succeeded;
* on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
/**
* If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting
* the container entity into a regular entity and putting it under the parent prefab, removing the link between this
* instance and the parent, removing links between this instance and it's nested instances, adding entities directly
* instance and the parent, removing links between this instance and its nested instances, and adding entities directly
* owned by this instance under the parent instance.
* Bails if the entity is not a container entity or belongs to the level prefab instance.
* @param containerEntityId The container entity id of the instance to detach.
@@ -25,6 +25,7 @@ namespace AzToolsFramework
{
using CreatePrefabResult = AZ::Outcome<AZ::EntityId, AZStd::string>;
using InstantiatePrefabResult = AZ::Outcome<AZ::EntityId, AZStd::string>;
using DuplicatePrefabResult = AZ::Outcome<EntityIdList, AZStd::string>;
using PrefabOperationResult = AZ::Outcome<void, AZStd::string>;
/**
@@ -69,6 +70,29 @@ namespace AzToolsFramework
* Return an outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0;
/**
* If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting
* the container entity into a regular entity and putting it under the parent prefab, removing the link between this
* instance and the parent, removing links between this instance and its nested instances, and adding entities directly
* owned by this instance under the parent instance.
* Bails if the entity is not a container entity or belongs to the level prefab instance.
* Return an outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) = 0;
/**
* Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance.
* Return an outcome object with a list of ids of given entities' duplicates if duplication succeeded;
* on failure, it comes with an error message detailing the cause of the error.
*/
virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
/**
* Get the file path to the prefab file for the prefab instance owning the entity provided.
* Returns the path to the prefab, or an empty path if the entity is owned by the level.
*/
virtual AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const = 0;
};
using PrefabPublicRequestBus = AZ::EBus<PrefabPublicRequests>;
@@ -28,6 +28,9 @@ namespace AzToolsFramework
->Event("CreatePrefabInMemory", &PrefabPublicRequests::CreatePrefabInMemory)
->Event("InstantiatePrefab", &PrefabPublicRequests::InstantiatePrefab)
->Event("DeleteEntitiesAndAllDescendantsInInstance", &PrefabPublicRequests::DeleteEntitiesAndAllDescendantsInInstance)
->Event("DetachPrefab", &PrefabPublicRequests::DetachPrefab)
->Event("DuplicateEntitiesInInstance", &PrefabPublicRequests::DuplicateEntitiesInInstance)
->Event("GetOwningInstancePrefabPath", &PrefabPublicRequests::GetOwningInstancePrefabPath)
;
}
}
@@ -62,5 +65,19 @@ namespace AzToolsFramework
return m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entityIds);
}
PrefabOperationResult PrefabPublicRequestHandler::DetachPrefab(const AZ::EntityId& containerEntityId)
{
return m_prefabPublicInterface->DetachPrefab(containerEntityId);
}
DuplicatePrefabResult PrefabPublicRequestHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
{
return m_prefabPublicInterface->DuplicateEntitiesInInstance(entityIds);
}
AZStd::string PrefabPublicRequestHandler::GetOwningInstancePrefabPath(AZ::EntityId entityId) const
{
return m_prefabPublicInterface->GetOwningInstancePrefabPath(entityId).Native();
}
} // namespace Prefab
} // namespace AzToolsFramework
@@ -34,6 +34,9 @@ namespace AzToolsFramework
CreatePrefabResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override;
InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override;
PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override;
DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const override;
private:
PrefabPublicInterface* m_prefabPublicInterface = nullptr;
@@ -23,7 +23,7 @@ namespace AzToolsFramework
}
//PrefabInstanceUndo
PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation)
PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation)
: PrefabUndoBase(undoOperationName)
{
m_useImmediatePropagation = useImmediatePropagation;
@@ -45,7 +45,7 @@ namespace AzToolsFramework
: public PrefabUndoBase
{
public:
explicit PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation = true);
explicit PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation = true);
void Capture(
const PrefabDom& initialState,
@@ -20,6 +20,8 @@
#include <AzFramework/Process/ProcessWatcher.h>
#include <AzToolsFramework/SourceControl/PerforceConnection.h>
#include <QProcess>
namespace AzToolsFramework
{
namespace
@@ -75,9 +77,17 @@ namespace AzToolsFramework
m_resolveKey = true;
m_testTrust = false;
// set up signals before we start thread.
m_shutdownThreadSignal = false;
m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this));
// Check to see if the 'p4' command is available at the command line
int p4VersionExitCode = QProcess::execute("p4", QStringList{ "-V" });
m_p4ApplicationDetected = (p4VersionExitCode == 0);
if (m_p4ApplicationDetected)
{
m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this));
}
SourceControlConnectionRequestBus::Handler::BusConnect();
SourceControlCommandBus::Handler::BusConnect();
@@ -88,10 +98,13 @@ namespace AzToolsFramework
SourceControlCommandBus::Handler::BusDisconnect();
SourceControlConnectionRequestBus::Handler::BusDisconnect();
m_shutdownThreadSignal = true; // tell the thread to die.
m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal
m_WorkerThread.join(); // wait for the thread to finish.
m_WorkerThread = AZStd::thread();
if (m_p4ApplicationDetected)
{
m_shutdownThreadSignal = true; // tell the thread to die.
m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal
m_WorkerThread.join(); // wait for the thread to finish.
m_WorkerThread = AZStd::thread();
}
SetConnection(nullptr);
}
@@ -260,5 +260,7 @@ namespace AzToolsFramework
AZStd::atomic_bool m_validConnection;
SourceControlState m_connectionState;
bool m_p4ApplicationDetected { false };
};
} // namespace AzToolsFramework
@@ -27,6 +27,7 @@
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabLoaderInterface.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
@@ -135,6 +136,10 @@ namespace AzToolsFramework
return;
}
// Initialize Editor functionality for the Prefab Focus Handler
auto prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
prefabFocusInterface->InitializeEditorInterfaces();
EditorContextMenuBus::Handler::BusConnect();
EditorEventsBus::Handler::BusConnect();
PrefabInstanceContainerNotificationBus::Handler::BusConnect();
@@ -250,7 +255,7 @@ namespace AzToolsFramework
if (s_prefabPublicInterface->IsInstanceContainerEntity(selectedEntity))
{
// Edit Prefab
if (prefabWipFeaturesEnabled && !s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity))
if (!s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity))
{
QAction* editAction = menu->addAction(QObject::tr("Edit Prefab"));
editAction->setToolTip(QObject::tr("Edit the prefab in focus mode."));
@@ -1154,25 +1159,14 @@ namespace AzToolsFramework
{
s_editorEntityUiInterface->RegisterEntity(entityId, m_prefabUiHandler.GetHandlerId());
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
if (prefabWipFeaturesEnabled)
{
// Register entity as a container
s_containerEntityInterface->RegisterEntityAsContainer(entityId);
}
// Register entity as a container
s_containerEntityInterface->RegisterEntityAsContainer(entityId);
}
}
void PrefabIntegrationManager::OnPrefabComponentDeactivate(AZ::EntityId entityId)
{
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
if (prefabWipFeaturesEnabled && !s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId))
if (!s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId))
{
// Unregister entity as a container
s_containerEntityInterface->UnregisterEntityAsContainer(entityId);
@@ -316,14 +316,7 @@ namespace AzToolsFramework
void PrefabUiHandler::OnDoubleClick(AZ::EntityId entityId) const
{
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
if (prefabWipFeaturesEnabled)
{
// Focus on this prefab
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
}
// Focus on this prefab
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
}
}
@@ -72,7 +72,7 @@ struct FolderRootWatch::PlatformImplementation
// Add the folder to watch and track it
int watchHandle = inotify_add_watch(m_iNotifyHandle,
cleanPath.toUtf8().constData(),
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
{
@@ -95,7 +95,7 @@ struct FolderRootWatch::PlatformImplementation
int watchHandle = inotify_add_watch(m_iNotifyHandle,
dirName.toUtf8().constData(),
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
{
@@ -10,7 +10,9 @@
#include <utilities/BatchApplicationManager.h>
#include <utilities/ApplicationServer.h>
#include <AzFramework/Asset/AssetSystemComponent.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/Utils/Utils.h>
#include <connection/connectionManager.h>
#include <QCoreApplication>
#include <QTemporaryDir>
@@ -98,10 +100,26 @@ namespace AssetProcessorMessagesTests
int argC = 0;
m_batchApplicationManager = AZStd::make_unique<UnitTestBatchApplicationManager>(&argC, nullptr, nullptr);
m_batchApplicationManager->BeforeRun();
// Override Game Name to be "AutomatedTesting"
AssetUtilities::ComputeProjectName("AutomatedTesting", true);
auto registry = AZ::SettingsRegistry::Get();
EXPECT_NE(registry, nullptr);
constexpr AZ::SettingsRegistryInterface::FixedValueString bootstrapKey{
AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey
};
constexpr AZ::SettingsRegistryInterface::FixedValueString projectPathKey{ bootstrapKey + "/project_path" };
registry->Set(projectPathKey, "AutomatedTesting");
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
// Force the branch token into settings registry before starting the application manager.
// This avoids writing the asset_processor.setreg file which can cause fileIO errors.
const AZ::IO::FixedMaxPathString enginePath = AZ::Utils::GetEnginePath();
constexpr AZ::SettingsRegistryInterface::FixedValueString branchTokenKey{ bootstrapKey + "/assetProcessor_branch_token" };
AZStd::string token;
AZ::StringFunc::AssetPath::CalculateBranchToken(enginePath.c_str(), token);
registry->Set(branchTokenKey, token.c_str());
auto status = m_batchApplicationManager->BeforeRun();
ASSERT_EQ(status, ApplicationManager::BeforeRunStatus::Status_Success);
m_batchApplicationManager->m_platformConfiguration = new PlatformConfiguration();
m_batchApplicationManager->InitAssetProcessorManager();
@@ -159,21 +177,25 @@ namespace AssetProcessorMessagesTests
ASSERT_TRUE(result);
});
}
void TearDown() override
{
QEventLoop eventLoop;
if (m_batchApplicationManager->m_connectionManager)
{
QEventLoop eventLoop;
QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit);
QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit);
m_batchApplicationManager->m_connectionManager->QuitRequested();
m_batchApplicationManager->m_connectionManager->QuitRequested();
eventLoop.exec();
eventLoop.exec();
}
m_assetSystemComponent->Deactivate();
if (m_assetSystemComponent)
{
m_assetSystemComponent->Deactivate();
}
m_batchApplicationManager->Destroy();
}
@@ -505,6 +505,14 @@ bool ApplicationManager::StartAZFramework()
AzFramework::Application::Descriptor appDescriptor;
AZ::ComponentApplication::StartupParameters params;
QDir projectPath{ AssetUtilities::ComputeProjectPath() };
if (!projectPath.exists("project.json"))
{
AZStd::string errorMsg = AZStd::string::format("Path '%s' is not a valid project path.", projectPath.path().toUtf8().constData());
AssetProcessor::MessageInfoBus::Broadcast(&AssetProcessor::MessageInfoBus::Events::OnErrorMessage, errorMsg.c_str());
return false;
}
QString projectName = AssetUtilities::ComputeProjectName();
// Prevent loading of gems in the Create method of the ComponentApplication
@@ -520,7 +528,6 @@ bool ApplicationManager::StartAZFramework()
//Registering all the Components
m_frameworkApp.RegisterComponentDescriptor(AzFramework::LogComponent::CreateDescriptor());
Reflect();
const AzFramework::CommandLine* commandLine = nullptr;
@@ -95,6 +95,8 @@ GUIApplicationManager::~GUIApplicationManager()
ApplicationManager::BeforeRunStatus GUIApplicationManager::BeforeRun()
{
AssetProcessor::MessageInfoBus::Handler::BusConnect();
ApplicationManager::BeforeRunStatus status = ApplicationManagerBase::BeforeRun();
if (status != ApplicationManager::BeforeRunStatus::Status_Success)
{
@@ -109,7 +111,6 @@ ApplicationManager::BeforeRunStatus GUIApplicationManager::BeforeRun()
#if defined(EXTERNAL_CRASH_REPORTING)
CrashHandler::ToolsCrashHandler::InitCrashHandler("AssetProcessor", projectAssetRoot.absolutePath().toStdString());
#endif
AssetProcessor::MessageInfoBus::Handler::BusConnect();
// we have to monitor both the cache folder and the database file and restart AP if either of them gets deleted
// It is important to note that we are monitoring the parent folder and not the actual cache folder itself since
@@ -436,98 +437,7 @@ bool GUIApplicationManager::OnError(const char* /*window*/, const char* message)
connection = Qt::QueuedConnection;
}
if (m_isCurrentlyLoadingGems)
{
// if something goes wrong during gem initialization, this is a special case and we need to be extra helpful.
const char* userSettingsFile = "_WAF_/user_settings.options";
const char* defaultSettingsFile = "_WAF_/default_settings.json";
QDir engineRoot;
AssetUtilities::ComputeEngineRoot(engineRoot);
QString settingsPath = engineRoot.absoluteFilePath(userSettingsFile);
QString friendlyErrorMessage;
bool usingDefaults = false;
if (QFile::exists(settingsPath))
{
QSettings loader(settingsPath, QSettings::IniFormat);
QVariant settingValue = loader.value("Game Projects/enabled_game_projects");
QStringList compiledProjects = settingValue.toStringList();
if (compiledProjects.isEmpty())
{
QByteArray byteArray;
QFile jsonFile;
jsonFile.setFileName(engineRoot.absoluteFilePath(defaultSettingsFile));
jsonFile.open(QIODevice::ReadOnly | QIODevice::Text);
byteArray = jsonFile.readAll();
jsonFile.close();
QJsonObject settingsObject = QJsonDocument::fromJson(byteArray).object();
QJsonArray projectsArray = settingsObject["Game Projects"].toArray();
if (!projectsArray.isEmpty())
{
auto projectObject = projectsArray[0].toObject();
QString projects = projectObject["default_value"].toString();
if (!projects.isEmpty())
{
compiledProjects = projects.split(',');
usingDefaults = true;
}
}
}
for (int i = 0; i < compiledProjects.size(); ++i)
{
compiledProjects[i] = compiledProjects[i].trimmed();
}
QString enabledProject = AssetUtilities::ComputeProjectName();
if (!compiledProjects.contains(enabledProject))
{
QString projectSourceLine;
if (usingDefaults)
{
projectSourceLine = QString("The currently compiled projects according to the defaults in %1 are '%2'").arg(defaultSettingsFile);
}
else
{
projectSourceLine = QString("The currently compiled projects according to %1 are '%2'").arg(userSettingsFile);
}
projectSourceLine = projectSourceLine.arg(compiledProjects.join(", "));
friendlyErrorMessage = QString("An error occurred while loading gems.\n"
"The enabled game project is not in the list of compiled projects.\n"
"Please configure the enabled project to be compiled and rebuild or change the enabled project.\n"
"The currently enabled game project (from bootstrap.cfg or /%4 command-line parameter) is '%1'.\n"
"%2\n"
"Full error text:\n"
"%3"
).arg(enabledProject).arg(projectSourceLine).arg(message).arg(AssetUtilities::ProjectPathOverrideParameter);
}
}
if (friendlyErrorMessage.isEmpty())
{
friendlyErrorMessage = QString("An error occurred while loading gems.\n"
"This can happen when new gems are added to a project, but those gems need to be built in order to function.\n"
"This can also happen when switching to a different project, one which uses gems which are not yet built.\n"
"To continue, please build the current project before attempting to run Asset Processor again.\n\n"
"Full error text:\n"
"%1").arg(message);
}
QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, friendlyErrorMessage), Q_ARG(bool, true));
}
else
{
QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, QString(message)), Q_ARG(bool, true));
}
QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, QString(message)), Q_ARG(bool, true));
return true;
}
@@ -5,75 +5,16 @@
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/std/string/string.h>
#include <AzFramework/Matchmaking/IMatchmakingRequests.h>
#include <AzFramework/Session/ISessionRequests.h>
namespace AWSGameLift
{
//! IAWSGameLiftRequests
//! GameLift Gem interfaces to configure client manager
class IAWSGameLiftRequests
{
public:
AZ_RTTI(IAWSGameLiftRequests, "{494167AD-1185-4AF3-8BF9-C8C37FC9C199}");
IAWSGameLiftRequests() = default;
virtual ~IAWSGameLiftRequests() = default;
//! ConfigureGameLiftClient
//! Configure GameLift client to interact with Amazon GameLift service
//! @param region Specifies the AWS region to use
//! @return True if client configuration succeeds, false otherwise
virtual bool ConfigureGameLiftClient(const AZStd::string& region) = 0;
//! CreatePlayerId
//! Create a new, random ID number for every player in every new game session.
//! @param includeBrackets Whether includes brackets in player id
//! @param includeDashes Whether includes dashes in player id
//! @return The player id to use in game session
virtual AZStd::string CreatePlayerId(bool includeBrackets, bool includeDashes) = 0;
};
// IAWSGameLiftRequests EBus wrapper for scripting
class AWSGameLiftRequests
: public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
};
using AWSGameLiftRequestBus = AZ::EBus<IAWSGameLiftRequests, AWSGameLiftRequests>;
// ISessionAsyncRequests EBus wrapper for scripting
class AWSGameLiftSessionAsyncRequests
: public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
};
using AWSGameLiftSessionAsyncRequestBus = AZ::EBus<AzFramework::ISessionAsyncRequests, AWSGameLiftSessionAsyncRequests>;
// ISessionRequests EBus wrapper for scripting
class AWSGameLiftSessionRequests
: public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
};
using AWSGameLiftSessionRequestBus = AZ::EBus<AzFramework::ISessionRequests, AWSGameLiftSessionRequests>;
// IMatchmakingAsyncRequests EBus wrapper for scripting
// IMatchmakingAsyncRequests EBus wrapper
class AWSGameLiftMatchmakingAsyncRequests
: public AZ::EBusTraits
{
@@ -84,7 +25,7 @@ namespace AWSGameLift
};
using AWSGameLiftMatchmakingAsyncRequestBus = AZ::EBus<AzFramework::IMatchmakingAsyncRequests, AWSGameLiftMatchmakingAsyncRequests>;
// IMatchmakingRequests EBus wrapper for scripting
// IMatchmakingRequests EBus wrapper
class AWSGameLiftMatchmakingRequests
: public AZ::EBusTraits
{
@@ -121,7 +62,7 @@ namespace AWSGameLift
virtual void StopPolling() = 0;
};
// IAWSGameLiftMatchmakingEventRequests EBus wrapper for scripting
// IAWSGameLiftMatchmakingEventRequests EBus wrapper
class AWSGameLiftMatchmakingEventRequests
: public AZ::EBusTraits
{
@@ -0,0 +1,51 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/std/string/string.h>
namespace AWSGameLift
{
//! IAWSGameLiftRequests
//! GameLift Gem interfaces to configure GameLift client and other help functions,
//! like creating random GameLift player id
class IAWSGameLiftRequests
{
public:
AZ_RTTI(IAWSGameLiftRequests, "{494167AD-1185-4AF3-8BF9-C8C37FC9C199}");
IAWSGameLiftRequests() = default;
virtual ~IAWSGameLiftRequests() = default;
//! ConfigureGameLiftClient
//! Configure GameLift client to interact with Amazon GameLift service
//! @param region Specifies the AWS region to use
//! @return True if client configuration succeeds, false otherwise
virtual bool ConfigureGameLiftClient(const AZStd::string& region) = 0;
//! CreatePlayerId
//! Create a new, random ID number for every player in every new game session.
//! @param includeBrackets Whether includes brackets in player id
//! @param includeDashes Whether includes dashes in player id
//! @return The player id to use in game session
virtual AZStd::string CreatePlayerId(bool includeBrackets, bool includeDashes) = 0;
};
// IAWSGameLiftRequests EBus wrapper
class AWSGameLiftRequests
: public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
};
using AWSGameLiftRequestBus = AZ::EBus<IAWSGameLiftRequests, AWSGameLiftRequests>;
} // namespace AWSGameLift
@@ -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/string/string.h>
#include <AzFramework/Session/ISessionRequests.h>
namespace AWSGameLift
{
// ISessionAsyncRequests EBus wrapper
class AWSGameLiftSessionAsyncRequests
: public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
};
using AWSGameLiftSessionAsyncRequestBus = AZ::EBus<AzFramework::ISessionAsyncRequests, AWSGameLiftSessionAsyncRequests>;
// ISessionRequests EBus wrapper
class AWSGameLiftSessionRequests
: public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
};
using AWSGameLiftSessionRequestBus = AZ::EBus<AzFramework::ISessionRequests, AWSGameLiftSessionRequests>;
} // namespace AWSGameLift
@@ -97,6 +97,7 @@ namespace AWSGameLift
AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName,
"Matchmaking ticket %s is complete.", ticket.GetTicketId().c_str());
RequestPlayerJoinMatch(ticket, playerId);
AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchComplete);
m_status = TicketTrackerStatus::Idle;
return;
}
@@ -104,25 +105,28 @@ namespace AWSGameLift
ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::FAILED ||
ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::CANCELLED)
{
AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, "Matchmaking ticket %s is not complete, %s",
ticket.GetTicketId().c_str(), ticket.GetStatusReason().c_str());
AZ_Warning(AWSGameLiftClientLocalTicketTrackerName, false, "Matchmaking ticket %s is not complete, %s",
ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str());
AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchFailure);
m_status = TicketTrackerStatus::Idle;
return;
}
else if (ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::REQUIRES_ACCEPTANCE)
{
// broadcast acceptance requires to player
AzFramework::MatchAcceptanceNotificationBus::Broadcast(&AzFramework::MatchAcceptanceNotifications::OnMatchAcceptance);
AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName, "Matchmaking ticket %s is pending on acceptance, %s.",
ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str());
AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchAcceptance);
}
else
{
AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName, "Matchmaking ticket %s is processing, %s.",
ticket.GetTicketId().c_str(), ticket.GetStatusReason().c_str());
ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str());
}
}
else
{
AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, "Unable to find expected ticket with id %s", ticketId.c_str());
AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError);
}
}
else
@@ -130,11 +134,13 @@ namespace AWSGameLift
AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, AWSGameLiftErrorMessageTemplate,
describeMatchmakingOutcome.GetError().GetExceptionName().c_str(),
describeMatchmakingOutcome.GetError().GetMessage().c_str());
AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError);
}
}
else
{
AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, AWSGameLiftClientMissingErrorMessage);
AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError);
}
m_waitEvent.try_acquire_for(AZStd::chrono::milliseconds(m_pollingPeriodInMS));
}
@@ -12,7 +12,7 @@
#include <AzCore/std/parallel/mutex.h>
#include <AzCore/std/parallel/thread.h>
#include <Request/IAWSGameLiftRequests.h>
#include <Request/AWSGameLiftMatchmakingRequestBus.h>
#include <aws/gamelift/model/MatchmakingTicket.h>
@@ -14,6 +14,7 @@
#include <AWSCoreBus.h>
#include <Credential/AWSCredentialBus.h>
#include <Framework/AWSApiJobConfig.h>
#include <ResourceMapping/AWSResourceMappingBus.h>
#include <AWSGameLiftClientManager.h>
@@ -75,7 +76,15 @@ namespace AWSGameLift
bool AWSGameLiftClientManager::ConfigureGameLiftClient(const AZStd::string& region)
{
AZ::Interface<IAWSGameLiftInternalRequests>::Get()->SetGameLiftClient(nullptr);
Aws::Client::ClientConfiguration clientConfig;
AWSCore::AwsApiJobConfig* defaultConfig = nullptr;
AWSCore::AWSCoreRequestBus::BroadcastResult(defaultConfig, &AWSCore::AWSCoreRequests::GetDefaultConfig);
if (defaultConfig)
{
clientConfig = defaultConfig->GetClientConfiguration();
}
// Set up client endpoint or region
AZStd::string localEndpoint = "";
#if defined(AWSGAMELIFT_DEV)
@@ -8,10 +8,13 @@
#pragma once
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AzFramework/Matchmaking/MatchmakingNotifications.h>
#include <Request/IAWSGameLiftRequests.h>
#include <Request/AWSGameLiftRequestBus.h>
#include <Request/AWSGameLiftSessionRequestBus.h>
#include <Request/AWSGameLiftMatchmakingRequestBus.h>
namespace AWSGameLift
{
@@ -23,22 +26,37 @@ namespace AWSGameLift
struct AWSGameLiftStartMatchmakingRequest;
struct AWSGameLiftStopMatchmakingRequest;
// MatchAcceptanceNotificationBus EBus handler for scripting
class AWSGameLiftMatchAcceptanceNotificationBusHandler
: public AzFramework::MatchAcceptanceNotificationBus::Handler
// MatchmakingNotificationBus EBus handler for scripting
class AWSGameLiftMatchmakingNotificationBusHandler
: public AzFramework::MatchmakingNotificationBus::Handler
, public AZ::BehaviorEBusHandler
{
public:
AZ_EBUS_BEHAVIOR_BINDER(
AWSGameLiftMatchAcceptanceNotificationBusHandler,
AWSGameLiftMatchmakingNotificationBusHandler,
"{CBE057D3-F5CE-46D3-B02D-8A6A1446B169}",
AZ::SystemAllocator,
OnMatchAcceptance);
OnMatchAcceptance, OnMatchComplete, OnMatchError, OnMatchFailure);
void OnMatchAcceptance() override
{
Call(FN_OnMatchAcceptance);
}
void OnMatchComplete() override
{
Call(FN_OnMatchComplete);
}
void OnMatchError() override
{
Call(FN_OnMatchError);
}
void OnMatchFailure() override
{
Call(FN_OnMatchFailure);
}
};
// MatchmakingAsyncRequestNotificationBus EBus handler for scripting
@@ -65,13 +65,6 @@ namespace AWSGameLift
->Event("CreatePlayerId", &AWSGameLiftRequestBus::Events::CreatePlayerId,
{ { { "IncludeBrackets", "" },
{ "IncludeDashes", "" } } });
behaviorContext->EBus<AWSGameLiftMatchmakingEventRequestBus>("AWSGameLiftMatchmakingEventRequestBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Event("StartPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StartPolling,
{ { { "TicketId", "" },
{ "PlayerId", "" } } })
->Event("StopPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StopPolling);
}
}
@@ -128,7 +121,7 @@ namespace AWSGameLift
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<AWSGameLiftMatchmakingAsyncRequestBus>("AWSGameLiftMatchmakingAsyncRequestBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking")
->Event("AcceptMatchAsync", &AWSGameLiftMatchmakingAsyncRequestBus::Events::AcceptMatchAsync,
{ { { "AcceptMatchRequest", "" } } })
->Event("StartMatchmakingAsync", &AWSGameLiftMatchmakingAsyncRequestBus::Events::StartMatchmakingAsync,
@@ -137,20 +130,28 @@ namespace AWSGameLift
{ { { "StopMatchmakingRequest", "" } } });
behaviorContext->EBus<AzFramework::MatchmakingAsyncRequestNotificationBus>("AWSGameLiftMatchmakingAsyncRequestNotificationBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking")
->Handler<AWSGameLiftMatchmakingAsyncRequestNotificationBusHandler>();
behaviorContext->EBus<AWSGameLiftMatchmakingRequestBus>("AWSGameLiftMatchmakingRequestBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Event("AcceptMatch", &AWSGameLiftMatchmakingRequestBus::Events::AcceptMatch, { { { "AcceptMatchRequest", "" } } })
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking")
->Event("AcceptMatch", &AWSGameLiftMatchmakingRequestBus::Events::AcceptMatch,
{ { { "AcceptMatchRequest", "" } } })
->Event("StartMatchmaking", &AWSGameLiftMatchmakingRequestBus::Events::StartMatchmaking,
{ { { "StartMatchmakingRequest", "" } } })
->Event("StopMatchmaking", &AWSGameLiftMatchmakingRequestBus::Events::StopMatchmaking,
{ { { "StopMatchmakingRequest", "" } } });
behaviorContext->EBus<AzFramework::MatchAcceptanceNotificationBus>("AWSGameLiftMatchAcceptanceNotificationBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Handler<AWSGameLiftMatchAcceptanceNotificationBusHandler>();
behaviorContext->EBus<AWSGameLiftMatchmakingEventRequestBus>("AWSGameLiftMatchmakingEventRequestBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking")
->Event("StartPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StartPolling,
{ { { "TicketId", "" },
{ "PlayerId", "" } } })
->Event("StopPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StopPolling);
behaviorContext->EBus<AzFramework::MatchmakingNotificationBus>("AWSGameLiftMatchmakingNotificationBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking")
->Handler<AWSGameLiftMatchmakingNotificationBusHandler>();
}
}
@@ -166,7 +167,7 @@ namespace AWSGameLift
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<AWSGameLiftSessionAsyncRequestBus>("AWSGameLiftSessionAsyncRequestBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session")
->Event("CreateSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::CreateSessionAsync,
{ { { "CreateSessionRequest", "" } } })
->Event("JoinSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::JoinSessionAsync, { { { "JoinSessionRequest", "" } } })
@@ -175,11 +176,11 @@ namespace AWSGameLift
->Event("LeaveSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::LeaveSessionAsync);
behaviorContext->EBus<AzFramework::SessionAsyncRequestNotificationBus>("AWSGameLiftSessionAsyncRequestNotificationBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session")
->Handler<AWSGameLiftSessionAsyncRequestNotificationBusHandler>();
behaviorContext->EBus<AWSGameLiftSessionRequestBus>("AWSGameLiftSessionRequestBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session")
->Event("CreateSession", &AWSGameLiftSessionRequestBus::Events::CreateSession, { { { "CreateSessionRequest", "" } } })
->Event("JoinSession", &AWSGameLiftSessionRequestBus::Events::JoinSession, { { { "JoinSessionRequest", "" } } })
->Event("SearchSessions", &AWSGameLiftSessionRequestBus::Events::SearchSessions, { { { "SearchSessionsRequest", "" } } })
@@ -20,7 +20,7 @@ namespace Aws
namespace AWSGameLift
{
//! IAWSGameLiftRequests
//! IAWSGameLiftInternalRequests
//! GameLift Gem internal interface which is used to fetch gem global GameLift client
class IAWSGameLiftInternalRequests
{
@@ -82,27 +82,12 @@ protected:
m_gameliftClientMockPtr.reset();
}
void WaitForProcessFinish(uint64_t expectedNum)
void WaitForProcessFinish(AZStd::function<bool()> processFinishCondition)
{
int processingTime = 0;
while (processingTime < TEST_WAIT_MAXIMUM_TIME_MS)
{
if (::UnitTest::TestRunner::Instance().m_numAssertsFailed == expectedNum)
{
AZ_TEST_STOP_TRACE_SUPPRESSION(expectedNum);
return;
}
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(TEST_WAIT_BUFFER_TIME_MS));
processingTime += TEST_WAIT_BUFFER_TIME_MS;
}
}
void WaitForProcessFinish()
{
int processingTime = 0;
while (processingTime < TEST_WAIT_MAXIMUM_TIME_MS)
{
if (m_gameliftClientTicketTracker->IsTrackerIdle())
if (processFinishCondition())
{
return;
}
@@ -119,19 +104,27 @@ public:
TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithoutClientSetup_GetExpectedErrors)
{
AZ::Interface<IAWSGameLiftInternalRequests>::Get()->SetGameLiftClient(nullptr);
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; });
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1);
ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_MultipleCallsWithoutClientSetup_GetExpectedErrors)
{
AZ::Interface<IAWSGameLiftInternalRequests>::Get()->SetGameLiftClient(nullptr);
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; });
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1);
ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -144,9 +137,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButWithFailedOu
.Times(1)
.WillOnce(::testing::Return(outcome));
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; });
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1);
ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -161,9 +157,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithMoreThanOne
.Times(1)
.WillOnce(::testing::Return(outcome));
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; });
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1);
ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -189,13 +188,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithCompleteSta
.Times(1)
.WillOnce(::testing::Return(outcome));
SessionHandlingClientRequestsMock handlerMock;
EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_))
SessionHandlingClientRequestsMock sessionHandlerMock;
EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_))
.Times(1)
.WillOnce(::testing::Return(true));
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish();
WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); });
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1);
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -217,9 +218,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButNoPlayerSess
.Times(1)
.WillOnce(::testing::Return(outcome));
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); });
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1);
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -245,14 +249,17 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButFailedToJoin
.Times(1)
.WillOnce(::testing::Return(outcome));
SessionHandlingClientRequestsMock handlerMock;
EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_))
SessionHandlingClientRequestsMock sessionHandlerMock;
EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_))
.Times(1)
.WillOnce(::testing::Return(false));
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
AZ_TEST_START_TRACE_SUPPRESSION;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); });
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1);
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -269,9 +276,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketTimeOu
.Times(1)
.WillOnce(::testing::Return(outcome));
AZ_TEST_START_TRACE_SUPPRESSION;
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); });
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1);
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -288,9 +296,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketFailed
.Times(1)
.WillOnce(::testing::Return(outcome));
AZ_TEST_START_TRACE_SUPPRESSION;
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); });
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1);
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -307,9 +316,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketCancel
.Times(1)
.WillOnce(::testing::Return(outcome));
AZ_TEST_START_TRACE_SUPPRESSION;
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish(1);
WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); });
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1);
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -342,13 +352,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallAndTicketComple
.WillOnce(::testing::Return(outcome1))
.WillOnce(::testing::Return(outcome2));
SessionHandlingClientRequestsMock handlerMock;
EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_))
SessionHandlingClientRequestsMock sessionHandlerMock;
EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_))
.Times(1)
.WillOnce(::testing::Return(true));
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish();
WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); });
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1);
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -365,7 +377,9 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_RequiresAcceptanceA
connectionInfo.SetIpAddress("DummyIpAddress");
connectionInfo.SetPort(123);
connectionInfo.AddMatchedPlayerSessions(
Aws::GameLift::Model::MatchedPlayerSession().WithPlayerId("player1").WithPlayerSessionId("playersession1"));
Aws::GameLift::Model::MatchedPlayerSession()
.WithPlayerId("player1")
.WithPlayerSessionId("playersession1"));
Aws::GameLift::Model::MatchmakingTicket ticket2;
ticket2.SetStatus(Aws::GameLift::Model::MatchmakingConfigurationStatus::COMPLETED);
@@ -379,13 +393,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_RequiresAcceptanceA
.WillOnce(::testing::Return(outcome1))
.WillOnce(::testing::Return(outcome2));
MatchAcceptanceNotificationsHandlerMock handlerMock1;
EXPECT_CALL(handlerMock1, OnMatchAcceptance()).Times(1);
SessionHandlingClientRequestsMock handlerMock2;
EXPECT_CALL(handlerMock2, RequestPlayerJoinSession(::testing::_)).Times(1).WillOnce(::testing::Return(true));
SessionHandlingClientRequestsMock sessionHandlerMock;
EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_))
.Times(1)
.WillOnce(::testing::Return(true));
MatchmakingNotificationsHandlerMock matchmakingHandlerMock;
m_gameliftClientTicketTracker->StartPolling("ticket1", "player1");
WaitForProcessFinish();
WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); });
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchAcceptance == 1);
ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1);
ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle());
}
@@ -266,6 +266,8 @@ const char* const AWSGameLiftClientManagerTest::DummyPlayerId = "dummyPlayerId";
TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutRegion_GetFalseAsResult)
{
AWSCoreRequestsHandlerMock coreHandlerMock;
EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr);
AZ_TEST_START_TRACE_SUPPRESSION;
auto result = m_gameliftClientManager->ConfigureGameLiftClient("");
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message
@@ -274,6 +276,8 @@ TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutRegion_G
TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutCredential_GetFalseAsResult)
{
AWSCoreRequestsHandlerMock coreHandlerMock;
EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr);
AWSResourceMappingRequestsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, GetDefaultRegion()).Times(1).WillOnce(::testing::Return("us-west-2"));
AZ_TEST_START_TRACE_SUPPRESSION;
@@ -284,6 +288,8 @@ TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutCredenti
TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithRegionAndCredential_GetTrueAsResult)
{
AWSCoreRequestsHandlerMock coreHandlerMock;
EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr);
AWSCredentialRequestsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, GetCredentialsProvider())
.Times(1)
@@ -9,9 +9,9 @@
#pragma once
#include <AzCore/Interface/Interface.h>
#include <AzFramework/Matchmaking/MatchmakingNotifications.h>
#include <AzFramework/Session/ISessionRequests.h>
#include <AzFramework/Session/ISessionHandlingRequests.h>
#include <AzFramework/Matchmaking/IMatchmakingRequests.h>
#include <AzFramework/Matchmaking/MatchmakingNotifications.h>
#include <AzTest/AzTest.h>
@@ -76,21 +76,44 @@ public:
MOCK_METHOD0(OnStopMatchmakingAsyncComplete, void());
};
class MatchAcceptanceNotificationsHandlerMock
: public AzFramework::MatchAcceptanceNotificationBus::Handler
class MatchmakingNotificationsHandlerMock
: public AzFramework::MatchmakingNotificationBus::Handler
{
public:
MatchAcceptanceNotificationsHandlerMock()
MatchmakingNotificationsHandlerMock()
{
AzFramework::MatchAcceptanceNotificationBus::Handler::BusConnect();
AzFramework::MatchmakingNotificationBus::Handler::BusConnect();
}
~MatchAcceptanceNotificationsHandlerMock()
~MatchmakingNotificationsHandlerMock()
{
AzFramework::MatchAcceptanceNotificationBus::Handler::BusDisconnect();
AzFramework::MatchmakingNotificationBus::Handler::BusDisconnect();
}
MOCK_METHOD0(OnMatchAcceptance, void());
void OnMatchAcceptance() override
{
++m_numMatchAcceptance;
}
void OnMatchComplete() override
{
++m_numMatchComplete;
}
void OnMatchError() override
{
++m_numMatchError;
}
void OnMatchFailure() override
{
++m_numMatchFailure;
}
int m_numMatchAcceptance = 0;
int m_numMatchComplete = 0;
int m_numMatchError = 0;
int m_numMatchFailure = 0;
};
class SessionAsyncRequestNotificationsHandlerMock
@@ -17,7 +17,9 @@ set(FILES
Include/Request/AWSGameLiftSearchSessionsRequest.h
Include/Request/AWSGameLiftStartMatchmakingRequest.h
Include/Request/AWSGameLiftStopMatchmakingRequest.h
Include/Request/IAWSGameLiftRequests.h
Include/Request/AWSGameLiftRequestBus.h
Include/Request/AWSGameLiftSessionRequestBus.h
Include/Request/AWSGameLiftMatchmakingRequestBus.h
Source/Activity/AWSGameLiftActivityUtils.cpp
Source/Activity/AWSGameLiftActivityUtils.h
Source/Activity/AWSGameLiftAcceptMatchActivity.cpp
@@ -5,7 +5,7 @@
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
@@ -45,7 +45,7 @@ namespace AWSGameLift
virtual bool StopMatchBackfill(const AZStd::string& ticketId) = 0;
};
// IAWSGameLiftServerRequests EBus wrapper for scripting
// IAWSGameLiftServerRequests EBus wrapper
class AWSGameLiftServerRequests
: public AZ::EBusTraits
{
@@ -20,7 +20,7 @@
#include <AzFramework/Session/SessionConfig.h>
#include <AWSGameLiftPlayer.h>
#include <Request/IAWSGameLiftServerRequests.h>
#include <Request/AWSGameLiftServerRequestBus.h>
namespace AWSGameLift
{
@@ -10,7 +10,7 @@ set(FILES
../AWSGameLiftCommon/Include/AWSGameLiftPlayer.h
../AWSGameLiftCommon/Source/AWSGameLiftPlayer.cpp
../AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h
Include/Request/IAWSGameLiftServerRequests.h
Include/Request/AWSGameLiftServerRequestBus.h
Source/AWSGameLiftServerManager.cpp
Source/AWSGameLiftServerManager.h
Source/AWSGameLiftServerSystemComponent.cpp
@@ -31,7 +31,7 @@ namespace AWSMetrics
static const unsigned int DesiredMaxWorkers = 2;
MetricsManager();
~MetricsManager();
virtual ~MetricsManager();
//! Initializing the metrics manager
//! @return Whether the operation is successful.
@@ -93,6 +93,12 @@ namespace AWSMetrics
//! @return Total number of requests for sending metrics events.
int GetNumTotalRequests() const;
protected:
//! Send metrics to a local file.
//! @param metricsQueue metricsQueue Metrics queue that stores the metrics.
//! @return Outcome of the operation.
virtual AZ::Outcome<void, AZStd::string> SendMetricsToFile(AZStd::shared_ptr<MetricsQueue> metricsQueue);
private:
//! Job management
void SetupJobContext();
@@ -112,11 +118,6 @@ namespace AWSMetrics
//! @param metricsQueue Metrics events to send.
void SendMetricsToServiceApiAsync(const MetricsQueue& metricsQueue);
//! Send metrics to a local file.
//! @param metricsQueue metricsQueue Metrics queue that stores the metrics.
//! @return Outcome of the operation.
AZ::Outcome<void, AZStd::string> SendMetricsToFile(AZStd::shared_ptr<MetricsQueue> metricsQueue);
//! Push metrics events to the front of the queue for retry.
//! @param metricsEventsForRetry Metrics events for retry.
void PushMetricsForRetry(MetricsQueue& metricsEventsForRetry);
@@ -139,11 +139,6 @@ namespace AWSMetrics
return true;
}
bool RemoveDirectory(const AZStd::string& directory)
{
return AZ::IO::SystemFile::DeleteDir(directory.c_str());
}
AZ::IO::FileIOBase* m_priorFileIO = nullptr;
AZ::IO::FileIOBase* m_localFileIO = nullptr;
@@ -75,6 +75,23 @@ namespace AZ
namespace AWSMetrics
{
class MetricsManagerMock
: public MetricsManager
{
private:
AZ::Outcome<void, AZStd::string> SendMetricsToFile(AZStd::shared_ptr<MetricsQueue> metricsQueue) override
{
if (AZ::IO::FileIOBase::GetInstance())
{
return AZ::Success();
}
else
{
return AZ::Failure(AZStd::string{ "Invalid File IO" });
}
}
};
class AWSMetricsNotificationBusMock
: protected AWSMetricsNotificationBus::Handler
{
@@ -134,13 +151,11 @@ namespace AWSMetrics
AWSMetricsGemAllocatorFixture::SetUp();
AWSMetricsRequestBus::Handler::BusConnect();
m_metricsManager = AZStd::make_unique<MetricsManager>();
m_metricsManager = AZStd::make_unique<MetricsManagerMock>();
AZStd::string configFilePath = CreateClientConfigFile(true, (double) TestMetricsEventSizeInBytes / MbToBytes * 2, DefaultFlushPeriodInSeconds, 0);
m_settingsRegistry->MergeSettingsFile(configFilePath, AZ::SettingsRegistryInterface::Format::JsonMergePatch, {});
m_metricsManager->Init();
RemoveFile(m_metricsManager->GetMetricsFilePath());
ReplaceLocalFileIOWithMockIO();
}
@@ -149,8 +164,6 @@ namespace AWSMetrics
RevertMockIOToLocalFileIO();
RemoveFile(GetDefaultTestFilePath());
RemoveFile(m_metricsManager->GetMetricsFilePath());
RemoveDirectory(m_metricsManager->GetMetricsFileDirectory());
m_metricsManager.reset();
@@ -233,7 +246,7 @@ namespace AWSMetrics
}
}
AZStd::unique_ptr<MetricsManager> m_metricsManager;
AZStd::unique_ptr<MetricsManagerMock> m_metricsManager;
AWSMetricsNotificationBusMock m_notifications;
AZ::IO::FileIOBase* m_fileIOMock;
@@ -138,7 +138,7 @@ float3 ColorGrade(float3 frameColor)
PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight);
frameColor = max(frameColor, 0.0);
frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation), PassSrg::m_colorAdjustmentWeight);
frameColor = max(frameColor, 0.0);
frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight,
PassSrg::m_splitToneShadowsColor, PassSrg::m_splitToneHighlightsColor);
frameColor = ColorGradeChannelMixer(frameColor, PassSrg::m_channelMixingRed, PassSrg::m_channelMixingGreen, PassSrg::m_channelMixingBlue);
@@ -147,8 +147,7 @@ float3 ColorGrade(float3 frameColor)
PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight,
PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor);
frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight);
frameColor = lerp(frameColor, ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift), PassSrg::m_finalAdjustmentWeight);
frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight);
return max(frameColor.rgb, 0.0);
}
@@ -111,16 +111,18 @@ void DirectionalLightShadow::GetShadowCoords(
float3 worldPosition,
out float3 shadowCoords[ViewSrg::MaxCascadeCount])
{
const float4x4 depthBiasMatrices[ViewSrg::MaxCascadeCount] =
ViewSrg::m_directionalLightShadows[lightIndex].m_depthBiasMatrices;
const uint cascadeCount = ViewSrg::m_directionalLightShadows[lightIndex].m_cascadeCount;
const float shadowBias = ViewSrg::m_directionalLightShadows[lightIndex].m_shadowBias;
const float4x4 lightViewToShadowmapMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_lightViewToShadowmapMatrices;
const float4x4 worldToLightViewMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_worldToLightViewMatrices;
for (uint index = 0; index < cascadeCount; ++index)
{
const float4x4 depthBiasMatrix = depthBiasMatrices[index];
const float4 shadowCoordHomogeneous = mul(depthBiasMatrix,
float4(worldPosition, 1.));
shadowCoords[index] = shadowCoordHomogeneous.xyz / shadowCoordHomogeneous.w;
float4 lightSpacePos = mul(worldToLightViewMatrices[index], float4(worldPosition, 1.));
lightSpacePos.z += shadowBias;
const float4 clipSpacePos = mul(lightViewToShadowmapMatrices[index], lightSpacePos);
shadowCoords[index] = clipSpacePos.xyz / clipSpacePos.w;
}
}
@@ -102,18 +102,19 @@ partial ShaderResourceGroup ViewSrg
struct DirectionalLightShadow
{
float4x4 m_depthBiasMatrices[MaxCascadeCount];
float4x4 m_lightViewToShadowmapMatrices[MaxCascadeCount];
float4x4 m_worldToLightViewMatrices[MaxCascadeCount];
float m_slopeBiasBase[MaxCascadeCount];
float m_boundaryScale;
uint m_shadowmapSize; // width and height of shadowmap
uint m_cascadeCount;
float m_shadowBias;
uint m_predictionSampleCount;
uint m_filteringSampleCount;
uint m_debugFlags;
uint m_shadowFilterMethod;
float m_far_minus_near;
float3 m_padding;
};
enum ShadowFilterMethod
@@ -157,6 +157,9 @@ namespace AZ
//! Sets whether the directional shadowmap should use receiver plane bias.
//! This attempts to reduce shadow acne when using large pcf filters.
virtual void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) = 0;
//! Reduces acne by applying a small amount of bias along shadow-space z.
virtual void SetShadowBias(LightHandle handle, float bias) = 0;
};
} // namespace Render
} // namespace AZ
@@ -589,6 +589,15 @@ namespace AZ
m_shadowProperties.GetData(handle.GetIndex()).m_isReceiverPlaneBiasEnabled = enable;
}
void DirectionalLightFeatureProcessor::SetShadowBias(LightHandle handle, float bias)
{
for (auto& it : m_shadowData)
{
it.second.GetData(handle.GetIndex()).m_shadowBias = bias;
}
m_shadowBufferNeedsUpdate = true;
}
void DirectionalLightFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline)
{
PrepareForChangingRenderPipelineAndCameraView();
@@ -1522,10 +1531,6 @@ namespace AZ
for (uint16_t cascadeIndex = 0; cascadeIndex < GetCascadeCount(handle); ++cascadeIndex)
{
const Matrix4x4& worldToLightClipMatrix = property.m_segments.at(cameraView)[cascadeIndex].m_view->GetWorldToClipMatrix();
const Matrix4x4 depthBiasMatrix = Shadow::GetClipToShadowmapTextureMatrix() * worldToLightClipMatrix;
shadowData.m_depthBiasMatrices[cascadeIndex] = depthBiasMatrix;
const Matrix4x4& lightViewToLightClipMatrix = property.m_segments.at(cameraView)[cascadeIndex].m_view->GetViewToClipMatrix();
const Matrix4x4 lightViewToShadowmapMatrix = Shadow::GetClipToShadowmapTextureMatrix() * lightViewToLightClipMatrix;
shadowData.m_lightViewToShadowmapMatrices[cascadeIndex] = lightViewToShadowmapMatrix;
@@ -72,12 +72,6 @@ namespace AZ
// [GFX TODO][ATOM-15172] Look into compacting struct DirectionalLightShadowData
struct DirectionalLightShadowData
{
AZStd::array<Matrix4x4, Shadow::MaxNumberOfCascades> m_depthBiasMatrices =
{ {
Matrix4x4::CreateIdentity(),
Matrix4x4::CreateIdentity(),
Matrix4x4::CreateIdentity(),
Matrix4x4::CreateIdentity() } };
AZStd::array<Matrix4x4, Shadow::MaxNumberOfCascades> m_lightViewToShadowmapMatrices =
{ {
Matrix4x4::CreateIdentity(),
@@ -97,11 +91,14 @@ namespace AZ
float m_boundaryScale = 0.f;
uint32_t m_shadowmapSize = 1; // width and height of shadowmap
uint32_t m_cascadeCount = 1;
// Reduce acne by applying a small amount of bias to apply along shadow-space z.
float m_shadowBias = 0.0f;
uint32_t m_predictionSampleCount = 0;
uint32_t m_filteringSampleCount = 0;
uint32_t m_debugFlags = 0;
uint32_t m_shadowFilterMethod = 0;
float m_far_minus_near = 0;
float m_padding[3];
};
class DirectionalLightFeatureProcessor final
@@ -218,6 +215,7 @@ namespace AZ
void SetShadowFilterMethod(LightHandle handle, ShadowFilterMethod method) override;
void SetFilteringSampleCount(LightHandle handle, uint16_t count) override;
void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) override;
void SetShadowBias(LightHandle handle, float bias) override;
const Data::Instance<RPI::Buffer> GetLightBuffer() const;
uint32_t GetLightCount() const;
@@ -10,6 +10,8 @@ if(NOT PAL_TRAIT_BUILD_HOST_TOOLS)
return()
endif()
ly_get_list_relative_pal_filename(pal_source_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/${PAL_PLATFORM_NAME})
ly_add_target(
NAME AtomToolsFramework.Static STATIC
NAMESPACE Gem
@@ -18,9 +20,11 @@ ly_add_target(
AUTORCC
FILES_CMAKE
atomtoolsframework_files.cmake
${pal_source_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
Source
${pal_source_dir}
PUBLIC
Include
BUILD_DEPENDENCIES
@@ -216,7 +216,11 @@ namespace AtomToolsFramework
AtomToolsMainWindowNotificationBus::Handler::BusDisconnect();
AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor);
#if AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY
_exit(0);
#else
Base::Destroy();
#endif
}
AZStd::vector<AZStd::string> AtomToolsApplication::GetCriticalAssetFilters() const
@@ -0,0 +1,15 @@
/*
* 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
// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of
// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit
// the application as soon as possible if set to true.
// (Tracked by GHI - 4806)
#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY true
@@ -5,7 +5,6 @@
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Module/Module.h>
AZ_DECLARE_MODULE_CLASS(Gem_AudioEngineWwise, AZ::Module)
#include <AtomToolsFramework_Traits_Linux.h>
@@ -0,0 +1,12 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
set(FILES
AtomToolsFramework_Traits_Platform.h
AtomToolsFramework_Traits_Linux.h
)
@@ -0,0 +1,15 @@
/*
* 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
// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of
// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit
// the application as soon as possible if set to true.
// (Tracked by GHI - 4806)
#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY false
@@ -0,0 +1,10 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AtomToolsFramework_Traits_Mac.h>
@@ -7,5 +7,6 @@
#
set(FILES
Source/AudioEngineWwiseModule_Stub.cpp
AtomToolsFramework_Traits_Platform.h
AtomToolsFramework_Traits_Mac.h
)
@@ -0,0 +1,10 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AtomToolsFramework_Traits_Windows.h>
@@ -0,0 +1,15 @@
/*
* 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
// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of
// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit
// the application as soon as possible if set to true.
// (Tracked by GHI - 4806)
#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY false
@@ -0,0 +1,12 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
set(FILES
AtomToolsFramework_Traits_Platform.h
AtomToolsFramework_Traits_Windows.h
)
@@ -168,6 +168,14 @@ namespace AZ
//! Sets whether the directional shadowmap should use receiver plane bias.
//! @param enable flag specifying whether to enable the receiver plane bias feature
virtual void SetShadowReceiverPlaneBiasEnabled(bool enable) = 0;
//! Shadow bias reduces acne by applying a small amount of offset along shadow-space z.
//! @return Returns the amount of bias to apply.
virtual float GetShadowBias() const = 0;
//! Shadow bias reduces acne by applying a small amount of offset along shadow-space z.
//! @param Sets the amount of bias to apply.
virtual void SetShadowBias(float bias) = 0;
};
using DirectionalLightRequestBus = EBus<DirectionalLightRequests>;
@@ -109,6 +109,9 @@ namespace AZ
//! This uses partial derivatives to reduce shadow acne when using large pcf kernels.
bool m_receiverPlaneBiasEnabled = true;
//! Reduces shadow acne by applying a small amount of offset along shadow-space z.
float m_shadowBias = 0.0f;
bool IsSplitManual() const;
bool IsSplitAutomatic() const;
bool IsCascadeCorrectionDisabled() const;
@@ -38,7 +38,8 @@ namespace AZ
->Field("IsDebugColoringEnabled", &DirectionalLightComponentConfig::m_isDebugColoringEnabled)
->Field("ShadowFilterMethod", &DirectionalLightComponentConfig::m_shadowFilterMethod)
->Field("PcfFilteringSampleCount", &DirectionalLightComponentConfig::m_filteringSampleCount)
->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled);
->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled)
->Field("Shadow Bias", &DirectionalLightComponentConfig::m_shadowBias);
}
}
@@ -84,6 +84,8 @@ namespace AZ
->Event("SetFilteringSampleCount", &DirectionalLightRequestBus::Events::SetFilteringSampleCount)
->Event("GetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::GetShadowReceiverPlaneBiasEnabled)
->Event("SetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::SetShadowReceiverPlaneBiasEnabled)
->Event("GetShadowBias", &DirectionalLightRequestBus::Events::GetShadowBias)
->Event("SetShadowBias", &DirectionalLightRequestBus::Events::SetShadowBias)
->VirtualProperty("Color", "GetColor", "SetColor")
->VirtualProperty("Intensity", "GetIntensity", "SetIntensity")
->VirtualProperty("AngularDiameter", "GetAngularDiameter", "SetAngularDiameter")
@@ -98,7 +100,8 @@ namespace AZ
->VirtualProperty("DebugColoringEnabled", "GetDebugColoringEnabled", "SetDebugColoringEnabled")
->VirtualProperty("ShadowFilterMethod", "GetShadowFilterMethod", "SetShadowFilterMethod")
->VirtualProperty("FilteringSampleCount", "GetFilteringSampleCount", "SetFilteringSampleCount")
->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled");
->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled")
->VirtualProperty("ShadowBias", "GetShadowBias", "SetShadowBias");
;
}
}
@@ -406,6 +409,20 @@ namespace AZ
return aznumeric_cast<uint32_t>(m_configuration.m_filteringSampleCount);
}
void DirectionalLightComponentController::SetShadowBias(float bias)
{
m_configuration.m_shadowBias = bias;
if (m_featureProcessor)
{
m_featureProcessor->SetShadowBias(m_lightHandle, bias);
}
}
float DirectionalLightComponentController::GetShadowBias() const
{
return m_configuration.m_shadowBias;
}
void DirectionalLightComponentController::SetFilteringSampleCount(uint32_t count)
{
const uint16_t count16 = GetMin(Shadow::MaxPcfSamplingCount, aznumeric_cast<uint16_t>(count));
@@ -499,6 +516,7 @@ namespace AZ
SetViewFrustumCorrectionEnabled(m_configuration.m_isCascadeCorrectionEnabled);
SetDebugColoringEnabled(m_configuration.m_isDebugColoringEnabled);
SetShadowFilterMethod(m_configuration.m_shadowFilterMethod);
SetShadowBias(m_configuration.m_shadowBias);
SetFilteringSampleCount(m_configuration.m_filteringSampleCount);
SetShadowReceiverPlaneBiasEnabled(m_configuration.m_receiverPlaneBiasEnabled);
@@ -80,6 +80,8 @@ namespace AZ
void SetFilteringSampleCount(uint32_t count) override;
bool GetShadowReceiverPlaneBiasEnabled() const override;
void SetShadowReceiverPlaneBiasEnabled(bool enable) override;
float GetShadowBias() const override;
void SetShadowBias(float width) override;
private:
friend class EditorDirectionalLightComponent;
@@ -133,8 +133,8 @@ namespace AZ
->EnumAttribute(ShadowFilterMethod::Esm, "ESM")
->EnumAttribute(ShadowFilterMethod::EsmPcf, "ESM+PCF")
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count",
"This is used only when the pixel is predicted as on the boundary. "
->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count\n",
"This is used only when the pixel is predicted as on the boundary.\n"
"Specific to PCF and ESM+PCF.")
->Attribute(Edit::Attributes::Min, 4)
->Attribute(Edit::Attributes::Max, 64)
@@ -142,10 +142,19 @@ namespace AZ
->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled)
->DataElement(
Edit::UIHandlers::CheckBox, &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled,
"Shadow Receiver Plane Bias Enable",
"Shadow Receiver Plane Bias Enable\n",
"This reduces shadow acne when using large pcf kernels.")
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled);
->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled)
->DataElement(
Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_shadowBias,
"Shadow Bias\n",
"Reduces acne by applying a fixed bias along z in shadow-space.\n"
"If this is 0, no biasing is applied.")
->Attribute(Edit::Attributes::Min, 0.f)
->Attribute(Edit::Attributes::Max, 0.2)
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
;
}
}

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