You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py

222 lines
11 KiB
Python

"""
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
"""
from __future__ import annotations
from collections import Counter
from collections import deque
from os import path
from PySide2 import QtWidgets
from azlmbr.entity import EntityId
from azlmbr.math import Vector3
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report
import azlmbr.bus as bus
import azlmbr.prefab as prefab
import editor_python_test_tools.pyside_utils as pyside_utils
import prefab.Prefab_Test_Utils as prefab_test_utils
# This is a helper class which contains some of the useful information about a prefab instance.
class PrefabInstance:
def __init__(self, name: str=None, prefab_file_name: str=None, container_entity: EditorEntity=EntityId()):
self.name = name
self.prefab_file_name: str = prefab_file_name
self.container_entity: EditorEntity = container_entity
"""
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() -> bool:
return self.container_entity.id.IsValid() and self.name is not None 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.
"""
async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId):
container_entity_name = self.container_entity.get_name()
current_children_entity_ids_having_prefab_name = prefab_test_utils.get_children_ids_by_name(parent_entity_id, container_entity_name)
Report.info(f'current_children_entity_ids_having_prefab_name: {current_children_entity_ids_having_prefab_name}')
pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id))
pyside_utils.run_soon(lambda: prefab_test_utils.wait_for_propagation())
try:
active_modal_widget = await pyside_utils.wait_for_modal_widget()
error_message_box = active_modal_widget.findChild(QtWidgets.QMessageBox)
ok_button = error_message_box.button(QtWidgets.QMessageBox.Ok)
ok_button.click()
assert False, "Cyclical dependency detected while reparenting prefab"
except pyside_utils.EventLoopTimeoutException:
pass
updated_children_entity_ids_having_prefab_name = prefab_test_utils.get_children_ids_by_name(parent_entity_id, container_entity_name)
Report.info(f'updated_children_entity_ids_having_prefab_name: {updated_children_entity_ids_having_prefab_name}')
new_child_with_reparented_prefab_name_added = len(updated_children_entity_ids_having_prefab_name) == len(current_children_entity_ids_having_prefab_name) + 1
assert new_child_with_reparented_prefab_name_added, "No entity with reparented prefab name become a child of target parent entity"
updated_container_entity_id = set(updated_children_entity_ids_having_prefab_name).difference(current_children_entity_ids_having_prefab_name).pop()
updated_container_entity = EditorEntity(updated_container_entity_id)
updated_container_entity_parent_id = updated_container_entity.get_parent_id()
has_correct_parent = updated_container_entity_parent_id.ToString() == parent_entity_id.ToString()
assert has_correct_parent, "Prefab reparented is *not* under the expected parent entity"
self.container_entity = EditorEntity(updated_container_entity_id)
# This is a helper class which contains some of the useful information about a prefab template.
class Prefab:
existing_prefabs = {}
def __init__(self, file_name: str):
self.file_name:str = file_name
self.file_path: str = prefab_test_utils.get_prefab_file_path(file_name)
self.instances: dict = {}
"""
Check if a prefab is ready to be used to generate its instances.
:param file_name: A unique file name of the target prefab.
:return: Whether the target prefab is loaded or not.
"""
@classmethod
def is_prefab_loaded(cls, file_name: str) -> bool:
return file_name 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_name: str) -> bool:
file_path = prefab_test_utils.get_prefab_file_path(file_name)
return path.exists(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:
if Prefab.is_prefab_loaded(file_name):
return Prefab.existing_prefabs[file_name]
else:
assert Prefab.prefab_exists(file_name), f"Attempted to get a prefab {file_name} that doesn't exist"
new_prefab = Prefab(file_name)
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: An outcome object with an entityId of the new prefab's container entity; on failure, it comes with an error message detailing the cause of the error.
"""
@classmethod
def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> 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)
entity_ids = [entity.id for entity in entities]
create_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'CreatePrefabInMemory', entity_ids, new_prefab.file_path)
assert create_prefab_result.IsSuccess(), f"Prefab operation 'CreatePrefab' failed. Error: {create_prefab_result.GetError()}"
container_entity = EditorEntity(create_prefab_result.GetValue())
if prefab_instance_name:
container_entity.set_name(prefab_instance_name)
else:
prefab_instance_name = file_name
prefab_test_utils.wait_for_propagation()
container_entity_id = prefab_test_utils.find_entity_by_unique_name(prefab_instance_name)
new_prefab.instances[prefab_instance_name] = PrefabInstance(prefab_instance_name, file_name, EditorEntity(container_entity_id))
Prefab.existing_prefabs[file_name] = new_prefab
return new_prefab
"""
Remove target prefab instances.
:param prefab_instances: Instances to be removed.
"""
@classmethod
def remove_prefabs(cls, prefab_instances: list[PrefabInstance]):
instances_to_remove_name_counts = Counter()
instances_removed_expected_name_counts = Counter()
entities_to_remove = [prefab_instance.container_entity for prefab_instance in prefab_instances]
while entities_to_remove:
entity = entities_to_remove.pop(-1)
entity_name = entity.get_name()
instances_to_remove_name_counts[entity_name] += 1
children_entity_ids = entity.get_children_ids()
for child_entity_id in children_entity_ids:
entities_to_remove.append(EditorEntity(child_entity_id))
for entity_name, entity_count in instances_to_remove_name_counts.items():
entities = prefab_test_utils.find_entities_by_name(entity_name)
instances_removed_expected_name_counts[entity_name] = len(entities) - entity_count
container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances]
delete_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DeleteEntitiesAndAllDescendantsInInstance', container_entity_ids)
assert delete_prefab_result.IsSuccess(), f"Prefab operation 'DeleteEntitiesAndAllDescendantsInInstance' failed. Error: {delete_prefab_result.GetError()}"
prefab_test_utils.wait_for_propagation()
prefab_entities_deleted = True
for entity_name, expected_entity_count in instances_removed_expected_name_counts.items():
actual_entity_count = len(prefab_test_utils.find_entities_by_name(entity_name))
if actual_entity_count is not expected_entity_count:
prefab_entities_deleted = False
break
assert prefab_entities_deleted, "Not all entities and descendants in target prefabs are deleted."
for instance in prefab_instances:
instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name)
instance_deleted_prefab.instances.pop(instance.name)
instance = PrefabInstance()
"""
Instantiate an instance of this prefab.
:param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name.
:param parent_entity: The entity the prefab should be a child of in the transform hierarchy.
:param prefab_position: The position in world space the prefab should be instantiated in.
:return: An outcome object with an entityId of the new prefab's container entity; on failure, it comes with an error message detailing the cause of the error.
"""
def instantiate(self, name: str=None, parent_entity: EditorEntity=None, prefab_position: Vector3=Vector3()) -> PrefabInstance:
parent_entity_id = parent_entity.id if parent_entity is not None else EntityId()
instantiate_prefab_result = prefab.PrefabPublicRequestBus(
bus.Broadcast, 'InstantiatePrefab', self.file_path, parent_entity_id, prefab_position)
assert instantiate_prefab_result.IsSuccess(), f"Prefab operation 'InstantiatePrefab' failed. Error: {instantiate_prefab_result.GetError()}"
container_entity_id = instantiate_prefab_result.GetValue()
container_entity = EditorEntity(container_entity_id)
if name:
container_entity.set_name(name)
else:
name = self.file_name
prefab_test_utils.wait_for_propagation()
container_entity_id = prefab_test_utils.find_entity_by_unique_name(name)
self.instances[name] = PrefabInstance(name, self.file_name, EditorEntity(container_entity_id))
prefab_test_utils.check_entity_at_position(container_entity_id, prefab_position)
return container_entity_id