Merge branch 'development' of https://github.com/aws-lumberyard-dev/o3de into scripting/rte_issues

This commit is contained in:
lsemp3d
2021-10-06 07:50:00 -07:00
99 changed files with 4456 additions and 203 deletions
@@ -0,0 +1,162 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
import os, traceback, binascii, sys, json, pathlib
import azlmbr.math
import azlmbr.bus
#
# SceneAPI Processor
#
def log_exception_traceback():
exc_type, exc_value, exc_tb = sys.exc_info()
data = traceback.format_exception(exc_type, exc_value, exc_tb)
print(str(data))
def get_mesh_node_names(sceneGraph):
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
meshDataList = []
node = sceneGraph.get_root()
children = []
paths = []
while node.IsValid():
# store children to process after siblings
if sceneGraph.has_node_child(node):
children.append(sceneGraph.get_node_child(node))
nodeName = sceneData.SceneGraphName(sceneGraph.get_node_name(node))
paths.append(nodeName.get_path())
# store any node that has mesh data content
nodeContent = sceneGraph.get_node_content(node)
if nodeContent.CastWithTypeName('MeshData'):
if sceneGraph.is_node_end_point(node) is False:
if (len(nodeName.get_path())):
meshDataList.append(sceneData.SceneGraphName(sceneGraph.get_node_name(node)))
# advance to next node
if sceneGraph.has_node_sibling(node):
node = sceneGraph.get_node_sibling(node)
elif children:
node = children.pop()
else:
node = azlmbr.scene.graph.NodeIndex()
return meshDataList, paths
def update_manifest(scene):
import json
import uuid, os
import azlmbr.scene as sceneApi
import azlmbr.scene.graph
from scene_api import scene_data as sceneData
graph = sceneData.SceneGraph(scene.graph)
# Get a list of all the mesh nodes, as well as all the nodes
mesh_name_list, all_node_paths = get_mesh_node_names(graph)
scene_manifest = sceneData.SceneManifest()
clean_filename = scene.sourceFilename.replace('.', '_')
# Compute the filename of the scene file
source_basepath = scene.watchFolder
source_relative_path = os.path.dirname(os.path.relpath(clean_filename, source_basepath))
source_filename_only = os.path.basename(clean_filename)
created_entities = []
# Loop every mesh node in the scene
for activeMeshIndex in range(len(mesh_name_list)):
mesh_name = mesh_name_list[activeMeshIndex]
mesh_path = mesh_name.get_path()
# Create a unique mesh group name using the filename + node name
mesh_group_name = '{}_{}'.format(source_filename_only, mesh_name.get_name())
# Remove forbidden filename characters from the name since this will become a file on disk later
mesh_group_name = "".join(char for char in mesh_group_name if char not in "|<>:\"/?*\\")
# Add the MeshGroup to the manifest and give it a unique ID
mesh_group = scene_manifest.add_mesh_group(mesh_group_name)
mesh_group['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, source_filename_only + mesh_path)) + '}'
# Set our current node as the only node that is included in this MeshGroup
scene_manifest.mesh_group_select_node(mesh_group, mesh_path)
# Explicitly remove all other nodes to prevent implicit inclusions
for node in all_node_paths:
if node != mesh_path:
scene_manifest.mesh_group_unselect_node(mesh_group, node)
# Create an editor entity
entity_id = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "CreateEditorReadyEntity", mesh_group_name)
# Add an EditorMeshComponent to the entity
editor_mesh_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "AZ::Render::EditorMeshComponent")
# Set the ModelAsset assetHint to the relative path of the input asset + the name of the MeshGroup we just created + the azmodel extension
# The MeshGroup we created will be output as a product in the asset's path named mesh_group_name.azmodel
# The assetHint will be converted to an AssetId later during prefab loading
json_update = json.dumps({
"Controller": { "Configuration": { "ModelAsset": {
"assetHint": os.path.join(source_relative_path, mesh_group_name) + ".azmodel" }}}
});
# Apply the JSON above to the component we created
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_mesh_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity failed")
# Keep track of the entity we set up, we'll add them all to the prefab we're creating later
created_entities.append(entity_id)
# Create a prefab with all our entities
prefab_filename = source_filename_only + ".prefab"
created_template_id = azlmbr.prefab.PrefabSystemScriptingBus(azlmbr.bus.Broadcast, "CreatePrefab", created_entities, prefab_filename)
if created_template_id == azlmbr.prefab.InvalidTemplateId:
raise RuntimeError("CreatePrefab {} failed".format(prefab_filename))
# Convert the prefab to a JSON string
output = azlmbr.prefab.PrefabLoaderScriptingBus(azlmbr.bus.Broadcast, "SaveTemplateToString", created_template_id)
if output.IsSuccess():
jsonString = output.GetValue()
uuid = azlmbr.math.Uuid_CreateRandom().ToString()
jsonResult = json.loads(jsonString)
# Add a PrefabGroup to the manifest and store the JSON on it
scene_manifest.add_prefab_group(source_filename_only, uuid, jsonResult)
else:
raise RuntimeError("SaveTemplateToString failed for template id {}, prefab {}".format(created_template_id, prefab_filename))
# Convert the manifest to a JSON string and return it
new_manifest = scene_manifest.export()
return new_manifest
sceneJobHandler = None
def on_update_manifest(args):
try:
scene = args[0]
return update_manifest(scene)
except RuntimeError as err:
print (f'ERROR - {err}')
log_exception_traceback()
global sceneJobHandler
sceneJobHandler = None
# try to create SceneAPI handler for processing
try:
import azlmbr.scene as sceneApi
if (sceneJobHandler == None):
sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
sceneJobHandler.connect()
sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
except:
sceneJobHandler = None
+1 -2
View File
@@ -21,7 +21,6 @@ set(ENABLED_GEMS
QtForPython
PythonAssetBuilder
Metastream
Camera
EMotionFX
AtomTressFX
@@ -53,6 +52,6 @@ set(ENABLED_GEMS
AWSCore
AWSClientAuth
AWSMetrics
PrefabBuilder
AudioSystem
)
@@ -65,4 +65,18 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_BUILD_TESTS_SUPPORTED AND AutomatedT
COMPONENT
Atom
)
ly_add_pytest(
NAME AutomatedTesting::Atom_TestSuite_Main_GPU_Optimized
TEST_SUITE main
TEST_REQUIRES gpu
TEST_SERIAL
TIMEOUT 1200
PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main_GPU_Optimized.py
RUNTIME_DEPENDENCIES
AssetProcessor
AutomatedTesting.Assets
Editor
COMPONENT
Atom
)
endif()
@@ -0,0 +1,43 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import os
import pytest
import ly_test_tools.environment.file_system as file_system
from ly_test_tools.o3de.editor_test import EditorSharedTest, EditorTestSuite
from ly_test_tools.image.screenshot_compare_qssim import qssim as compare_screenshots
from .atom_utils.atom_component_helper import create_screenshots_archive, golden_images_directory
DEFAULT_SUBFOLDER_PATH = 'user/PythonTests/Automated/Screenshots'
@pytest.mark.xfail(reason="Optimized tests are experimental, we will enable xfail and monitor them temporarily.")
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
class TestAutomation(EditorTestSuite):
# Remove -autotest_mode from global_extra_cmdline_args since we need rendering for these tests.
global_extra_cmdline_args = ["-BatchMode"] # Default is ["-BatchMode", "-autotest_mode"]
class AtomGPU_BasicLevelSetup_SetsUpLevel(EditorSharedTest):
use_null_renderer = False # Default is True
screenshot_name = "AtomBasicLevelSetup.ppm"
test_screenshots = [] # Gets set by setup()
screenshot_directory = "" # Gets set by setup()
# Clear existing test screenshots before starting test.
def setup(self, workspace):
screenshot_directory = os.path.join(workspace.paths.project(), DEFAULT_SUBFOLDER_PATH)
test_screenshots = [os.path.join(screenshot_directory, self.screenshot_name)]
file_system.delete(test_screenshots, True, True)
from Atom.tests import hydra_AtomGPU_BasicLevelSetup as test_module
golden_images = [os.path.join(golden_images_directory(), screenshot_name)]
for test_screenshot, golden_screenshot in zip(test_screenshots, golden_images):
compare_screenshots(test_screenshot, golden_screenshot)
create_screenshots_archive(screenshot_directory)
@@ -3,13 +3,54 @@ Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright
SPDX-License-Identifier: Apache-2.0 OR MIT
File to assist with common hydra component functions used across various Atom tests.
"""
import datetime
import os
import zipfile
from editor_python_test_tools.editor_test_helper import EditorTestHelper
helper = EditorTestHelper(log_prefix="Atom_EditorTestHelper")
def create_screenshots_archive(screenshot_path):
"""
Creates a new zip file archive at archive_path containing all files listed within archive_path.
:param screenshot_path: location containing the files to archive, the zip archive file will also be saved here.
:return: None, but creates a new zip file archive inside path containing all of the files inside archive_path.
"""
files_to_archive = []
# Search for .png and .ppm files to add to the zip archive file.
for (folder_name, sub_folders, file_names) in os.walk(screenshot_path):
for file_name in file_names:
if file_name.endswith(".png") or file_name.endswith(".ppm"):
file_path = os.path.join(folder_name, file_name)
files_to_archive.append(file_path)
# Setup variables for naming the zip archive file.
timestamp = datetime.datetime.now().timestamp()
formatted_timestamp = datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d_%H-%M-%S")
screenshots_file = os.path.join(screenshot_path, f'screenshots_{formatted_timestamp}.zip')
# Write all of the valid .png and .ppm files to the archive file.
with zipfile.ZipFile(screenshots_file, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as zip_archive:
for file_path in files_to_archive:
file_name = os.path.basename(file_path)
zip_archive.write(file_path, file_name)
def golden_images_directory():
"""
Uses this file location to return the valid location for golden image files.
:return: The path to the golden_images directory, but raises an IOError if the golden_images directory is missing.
"""
current_file_directory = os.path.join(os.path.dirname(__file__))
golden_images_dir = os.path.join(current_file_directory, '..', 'golden_images')
if not os.path.exists(golden_images_dir):
raise IOError(
f'golden_images" directory was not found at path "{golden_images_dir}"'
f'Please add a "golden_images" directory inside: "{current_file_directory}"'
)
return golden_images_dir
def create_basic_atom_level(level_name):
@@ -31,6 +72,9 @@ def create_basic_atom_level(level_name):
import azlmbr.object
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.editor_test_helper import EditorTestHelper
helper = EditorTestHelper(log_prefix="Atom_EditorTestHelper")
# Create a new level.
new_level_name = level_name
@@ -0,0 +1,292 @@
"""
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
"""
# fmt: off
class Tests :
camera_component_added = ("Camera component was added", "Camera component wasn't added")
camera_fov_set = ("Camera component FOV property set", "Camera component FOV property wasn't set")
directional_light_component_added = ("Directional Light component added", "Directional Light component wasn't added")
enter_game_mode = ("Entered game mode", "Failed to enter game mode")
exit_game_mode = ("Exited game mode", "Couldn't exit game mode")
global_skylight_component_added = ("Global Skylight (IBL) component added", "Global Skylight (IBL) component wasn't added")
global_skylight_diffuse_image_set = ("Global Skylight Diffuse Image property set", "Global Skylight Diffuse Image property wasn't set")
global_skylight_specular_image_set = ("Global Skylight Specular Image property set", "Global Skylight Specular Image property wasn't set")
ground_plane_material_asset_set = ("Ground Plane Material Asset was set", "Ground Plane Material Asset wasn't set")
ground_plane_material_component_added = ("Ground Plane Material component added", "Ground Plane Material component wasn't added")
ground_plane_mesh_asset_set = ("Ground Plane Mesh Asset property was set", "Ground Plane Mesh Asset property wasn't set")
hdri_skybox_component_added = ("HDRi Skybox component added", "HDRi Skybox component wasn't added")
hdri_skybox_cubemap_texture_set = ("HDRi Skybox Cubemap Texture property set", "HDRi Skybox Cubemap Texture property wasn't set")
mesh_component_added = ("Mesh component added", "Mesh component wasn't added")
no_assert_occurred = ("No asserts detected", "Asserts were detected")
no_error_occurred = ("No errors detected", "Errors were detected")
secondary_grid_spacing = ("Secondary Grid Spacing set", "Secondary Grid Spacing not set")
sphere_material_component_added = ("Sphere Material component added", "Sphere Material component wasn't added")
sphere_material_set = ("Sphere Material Asset was set", "Sphere Material Asset wasn't set")
sphere_mesh_asset_set = ("Sphere Mesh Asset was set", "Sphere Mesh Asset wasn't set")
viewport_set = ("Viewport set to correct size", "Viewport not set to correct size")
# fmt: on
def AtomGPU_BasicLevelSetup_SetsUpLevel():
"""
Summary:
Sets up a level to match the AtomBasicLevelSetup.ppm golden image then takes a screenshot to verify the setup.
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
Expected Behavior:
The scene can be setup for a basic level.
The test screenshot matches the appearance of the AtomBasicLevelSetup.ppm golden image.
Test Steps:
1. Close error windows and display helpers then update the viewport size.
2. Create Default Level Entity.
3. Create Grid Entity as a child entity of the Default Level Entity.
4. Add Grid component to Grid Entity and set Secondary Grid Spacing.
5. Create Global Skylight (IBL) Entity as a child entity of the Default Level Entity.
6. Add HDRi Skybox component to the Global Skylight (IBL) Entity.
7. Add Global Skylight (IBL) component to the Global Skylight (IBL) Entity.
8. Set the Cubemap Texture property of the HDRi Skybox component.
9. Set the Diffuse Image property of the Global Skylight (IBL) component.
10. Set the Specular Image property of the Global Skylight (IBL) component.
11. Create a Ground Plane Entity with a Material component that is a child entity of the Default Level Entity.
12. Set the Material Asset property of the Material component for the Ground Plane Entity.
13. Add the Mesh component to the Ground Plane Entity and set the Mesh component Mesh Asset property.
14. Create a Directional Light Entity as a child entity of the Default Level Entity.
15. Add Directional Light component to Directional Light Entity and set entity rotation.
16. Create a Sphere Entity as a child entity of the Default Level Entity then add a Material component.
17. Set the Material Asset property of the Material component for the Sphere Entity.
18. Add Mesh component to Sphere Entity and set the Mesh Asset property for the Mesh component.
19. Create a Camera Entity as a child entity of the Default Level Entity then add a Camera component.
20. Set the Camera Entity rotation value and set the Camera component Field of View value.
21. Enter game mode.
22. Take screenshot.
23. Exit game mode.
24. Look for errors.
:return: None
"""
import os
from math import isclose
import azlmbr.asset as asset
import azlmbr.bus as bus
import azlmbr.legacy.general as general
import azlmbr.math as math
import azlmbr.paths
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper as helper
from Atom.atom_utils.screenshot_utils import ScreenshotHelper
MATERIAL_COMPONENT_NAME = "Material"
MESH_COMPONENT_NAME = "Mesh"
SCREENSHOT_NAME = "AtomBasicLevelSetup"
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
DEGREE_RADIAN_FACTOR = 0.0174533
def initial_viewport_setup(screen_width, screen_height):
general.set_viewport_size(screen_width, screen_height)
general.update_viewport()
result = isclose(
a=general.get_viewport_size().x, b=SCREEN_WIDTH, rel_tol=0.1) and isclose(
a=general.get_viewport_size().y, b=SCREEN_HEIGHT, rel_tol=0.1)
return result
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
helper.init_idle()
helper.open_level("", "Base")
# Test steps begin.
# 1. Close error windows and display helpers then update the viewport size.
helper.close_error_windows()
helper.close_display_helpers()
general.update_viewport()
Report.critical_result(Tests.viewport_set, initial_viewport_setup(SCREEN_WIDTH, SCREEN_HEIGHT))
# 2. Create Default Level Entity.
default_level_entity_name = "Default Level"
default_level_entity = EditorEntity.create_editor_entity_at(
math.Vector3(0.0, 0.0, 0.0), default_level_entity_name)
# 3. Create Grid Entity as a child entity of the Default Level Entity.
grid_name = "Grid"
grid_entity = EditorEntity.create_editor_entity(grid_name, default_level_entity.id)
# 4. Add Grid component to Grid Entity and set Secondary Grid Spacing.
grid_component = grid_entity.add_component(grid_name)
secondary_grid_spacing_property = "Controller|Configuration|Secondary Grid Spacing"
secondary_grid_spacing_value = 1.0
grid_component.set_component_property_value(secondary_grid_spacing_property, secondary_grid_spacing_value)
secondary_grid_spacing_set = grid_component.get_component_property_value(
secondary_grid_spacing_property) == secondary_grid_spacing_value
Report.result(Tests.secondary_grid_spacing, secondary_grid_spacing_set)
# 5. Create Global Skylight (IBL) Entity as a child entity of the Default Level Entity.
global_skylight_name = "Global Skylight (IBL)"
global_skylight_entity = EditorEntity.create_editor_entity(global_skylight_name, default_level_entity.id)
# 6. Add HDRi Skybox component to the Global Skylight (IBL) Entity.
hdri_skybox_name = "HDRi Skybox"
hdri_skybox_component = global_skylight_entity.add_component(hdri_skybox_name)
Report.result(Tests.hdri_skybox_component_added, global_skylight_entity.has_component(hdri_skybox_name))
# 7. Add Global Skylight (IBL) component to the Global Skylight (IBL) Entity.
global_skylight_component = global_skylight_entity.add_component(global_skylight_name)
Report.result(Tests.global_skylight_component_added, global_skylight_entity.has_component(global_skylight_name))
# 8. Set the Cubemap Texture property of the HDRi Skybox component.
global_skylight_image_asset_path = os.path.join(
"LightingPresets", "greenwich_park_02_4k_iblskyboxcm_iblspecular.exr.streamingimage")
global_skylight_image_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", global_skylight_image_asset_path, math.Uuid(), False)
hdri_skybox_cubemap_texture_property = "Controller|Configuration|Cubemap Texture"
hdri_skybox_component.set_component_property_value(
hdri_skybox_cubemap_texture_property, global_skylight_image_asset)
Report.result(
Tests.hdri_skybox_cubemap_texture_set,
hdri_skybox_component.get_component_property_value(
hdri_skybox_cubemap_texture_property) == global_skylight_image_asset)
# 9. Set the Diffuse Image property of the Global Skylight (IBL) component.
# Re-use the same image that was used in the previous test step.
global_skylight_diffuse_image_property = "Controller|Configuration|Diffuse Image"
global_skylight_component.set_component_property_value(
global_skylight_diffuse_image_property, global_skylight_image_asset)
Report.result(
Tests.global_skylight_diffuse_image_set,
global_skylight_component.get_component_property_value(
global_skylight_diffuse_image_property) == global_skylight_image_asset)
# 10. Set the Specular Image property of the Global Skylight (IBL) component.
# Re-use the same image that was used in the previous test step.
global_skylight_specular_image_property = "Controller|Configuration|Specular Image"
global_skylight_component.set_component_property_value(
global_skylight_specular_image_property, global_skylight_image_asset)
global_skylight_specular_image_set = global_skylight_component.get_component_property_value(
global_skylight_specular_image_property)
Report.result(
Tests.global_skylight_specular_image_set, global_skylight_specular_image_set == global_skylight_image_asset)
# 11. Create a Ground Plane Entity with a Material component that is a child entity of the Default Level Entity.
ground_plane_name = "Ground Plane"
ground_plane_entity = EditorEntity.create_editor_entity(ground_plane_name, default_level_entity.id)
ground_plane_material_component = ground_plane_entity.add_component(MATERIAL_COMPONENT_NAME)
Report.result(
Tests.ground_plane_material_component_added, ground_plane_entity.has_component(MATERIAL_COMPONENT_NAME))
# 12. Set the Material Asset property of the Material component for the Ground Plane Entity.
ground_plane_entity.set_local_uniform_scale(32.0)
ground_plane_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_chrome.azmaterial")
ground_plane_material_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", ground_plane_material_asset_path, math.Uuid(), False)
ground_plane_material_asset_property = "Default Material|Material Asset"
ground_plane_material_component.set_component_property_value(
ground_plane_material_asset_property, ground_plane_material_asset)
Report.result(
Tests.ground_plane_material_asset_set,
ground_plane_material_component.get_component_property_value(
ground_plane_material_asset_property) == ground_plane_material_asset)
# 13. Add the Mesh component to the Ground Plane Entity and set the Mesh component Mesh Asset property.
ground_plane_mesh_component = ground_plane_entity.add_component(MESH_COMPONENT_NAME)
Report.result(Tests.mesh_component_added, ground_plane_entity.has_component(MESH_COMPONENT_NAME))
ground_plane_mesh_asset_path = os.path.join("Objects", "plane.azmodel")
ground_plane_mesh_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", ground_plane_mesh_asset_path, math.Uuid(), False)
ground_plane_mesh_asset_property = "Controller|Configuration|Mesh Asset"
ground_plane_mesh_component.set_component_property_value(
ground_plane_mesh_asset_property, ground_plane_mesh_asset)
Report.result(
Tests.ground_plane_mesh_asset_set,
ground_plane_mesh_component.get_component_property_value(
ground_plane_mesh_asset_property) == ground_plane_mesh_asset)
# 14. Create a Directional Light Entity as a child entity of the Default Level Entity.
directional_light_name = "Directional Light"
directional_light_entity = EditorEntity.create_editor_entity_at(
math.Vector3(0.0, 0.0, 10.0), directional_light_name, default_level_entity.id)
# 15. Add Directional Light component to Directional Light Entity and set entity rotation.
directional_light_entity.add_component(directional_light_name)
directional_light_entity_rotation = math.Vector3(DEGREE_RADIAN_FACTOR * -90.0, 0.0, 0.0)
directional_light_entity.set_local_rotation(directional_light_entity_rotation)
Report.result(
Tests.directional_light_component_added, directional_light_entity.has_component(directional_light_name))
# 16. Create a Sphere Entity as a child entity of the Default Level Entity then add a Material component.
sphere_entity = EditorEntity.create_editor_entity_at(
math.Vector3(0.0, 0.0, 1.0), "Sphere", default_level_entity.id)
sphere_material_component = sphere_entity.add_component(MATERIAL_COMPONENT_NAME)
Report.result(Tests.sphere_material_component_added, sphere_entity.has_component(MATERIAL_COMPONENT_NAME))
# 17. Set the Material Asset property of the Material component for the Sphere Entity.
sphere_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_brass_polished.azmaterial")
sphere_material_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", sphere_material_asset_path, math.Uuid(), False)
sphere_material_asset_property = "Default Material|Material Asset"
sphere_material_component.set_component_property_value(sphere_material_asset_property, sphere_material_asset)
Report.result(Tests.sphere_material_set, sphere_material_component.get_component_property_value(
sphere_material_asset_property) == sphere_material_asset)
# 18. Add Mesh component to Sphere Entity and set the Mesh Asset property for the Mesh component.
sphere_mesh_component = sphere_entity.add_component(MESH_COMPONENT_NAME)
sphere_mesh_asset_path = os.path.join("Models", "sphere.azmodel")
sphere_mesh_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, "GetAssetIdByPath", sphere_mesh_asset_path, math.Uuid(), False)
sphere_mesh_asset_property = "Controller|Configuration|Mesh Asset"
sphere_mesh_component.set_component_property_value(sphere_mesh_asset_property, sphere_mesh_asset)
Report.result(Tests.sphere_mesh_asset_set, sphere_mesh_component.get_component_property_value(
sphere_mesh_asset_property) == sphere_mesh_asset)
# 19. Create a Camera Entity as a child entity of the Default Level Entity then add a Camera component.
camera_name = "Camera"
camera_entity = EditorEntity.create_editor_entity_at(
math.Vector3(5.5, -12.0, 9.0), camera_name, default_level_entity.id)
camera_component = camera_entity.add_component(camera_name)
Report.result(Tests.camera_component_added, camera_entity.has_component(camera_name))
# 20. Set the Camera Entity rotation value and set the Camera component Field of View value.
camera_entity_rotation = math.Vector3(
DEGREE_RADIAN_FACTOR * -27.0, DEGREE_RADIAN_FACTOR * -12.0, DEGREE_RADIAN_FACTOR * 25.0)
camera_entity.set_local_rotation(camera_entity_rotation)
camera_fov_property = "Controller|Configuration|Field of view"
camera_fov_value = 60.0
camera_component.set_component_property_value(camera_fov_property, camera_fov_value)
azlmbr.camera.EditorCameraViewRequestBus(azlmbr.bus.Event, "ToggleCameraAsActiveView", camera_entity.id)
Report.result(Tests.camera_fov_set, camera_component.get_component_property_value(
camera_fov_property) == camera_fov_value)
# 21. Enter game mode.
helper.enter_game_mode(Tests.enter_game_mode)
helper.wait_for_condition(function=lambda: general.is_in_game_mode(), timeout_in_seconds=4.0)
# 22. Take screenshot.
ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(f"{SCREENSHOT_NAME}.ppm")
# 23. Exit game mode.
helper.exit_game_mode(Tests.exit_game_mode)
helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=4.0)
# 24. Look for errors.
helper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
Report.result(Tests.no_assert_occurred, not error_tracer.has_asserts)
Report.result(Tests.no_error_occurred, not error_tracer.has_errors)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomGPU_BasicLevelSetup_SetsUpLevel)
@@ -361,3 +361,19 @@ class EditorEntity:
:return: True if "isVisible" is enabled, False otherwise.
"""
return editor.EditorEntityInfoRequestBus(bus.Event, "IsVisible", self.id)
def set_local_uniform_scale(self, scale_float) -> None:
"""
Sets the "SetLocalUniformScale" value on the entity.
:param scale_float: value for "SetLocalUniformScale" to set to.
:return: None
"""
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalUniformScale", self.id, scale_float)
def set_local_rotation(self, vector3_rotation) -> None:
"""
Sets the "SetLocalRotation" value on the entity.
:param vector3_rotation: The math.Vector3 value to use for rotation on the entity (uses radians).
:return: None
"""
azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", self.id, vector3_rotation)
@@ -4,18 +4,18 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import json
import math
import os
import time
import math
import traceback
from typing import Callable, Tuple
import azlmbr
import azlmbr.legacy.general as general
import azlmbr.debug
import json
import traceback
from typing import Callable, Tuple
class FailFast(Exception):
"""
@@ -127,6 +127,31 @@ class TestHelper:
if ret:
return True
@staticmethod
def close_error_windows():
"""
Closes Error Report and Error Log windows that block focus if they are visible.
:return: None
"""
if general.is_pane_visible("Error Report"):
general.close_pane("Error Report")
if general.is_pane_visible("Error Log"):
general.close_pane("Error Log")
@staticmethod
def close_display_helpers():
"""
Closes helper gizmos, anti-aliasing, and FPS meters.
:return: None
"""
if general.is_helpers_shown():
general.toggle_helpers()
general.idle_wait(1.0)
general.idle_wait(1.0)
general.run_console("r_displayInfo=0")
general.run_console("r_antialiasingmode=0")
general.idle_wait(1.0)
class Timeout:
# type: (float) -> None
@@ -149,6 +174,7 @@ class Timeout:
def timed_out(self):
return time.time() > self.die_after
class Report:
_results = []
_exception = None
@@ -290,8 +316,8 @@ class Report:
Report.info(" x: {:.2f}, y: {:.2f}, z: {:.2f}".format(vector3.x, vector3.y, vector3.z))
if magnitude is not None:
Report.info(" magnitude: {:.2f}".format(magnitude))
'''
Utility for scope tracing errors and warnings.
Usage:
@@ -303,7 +329,7 @@ Usage:
Report.result(Tests.warnings_not_found_in_section, not section_tracer.has_warnings)
'''
'''
class Tracer:
def __init__(self):
self.warnings = []
@@ -349,10 +375,10 @@ class Tracer:
self.line = args[1]
self.function = args[2]
self.message = args[3]
def __str__(self):
return f"Assert: [{self.filename}:{self.function}:{self.line}]: {self.message}"
def __repr__(self):
return f"[Assert: {self.message}]"
@@ -360,21 +386,21 @@ class Tracer:
def __init__(self, args):
self.window = args[0]
self.message = args[1]
def _on_warning(self, args):
warningInfo = Tracer.WarningInfo(args)
self.warnings.append(warningInfo)
Report.info("Tracer caught Warning: %s" % warningInfo.message)
self.has_warnings = True
return False
def _on_error(self, args):
errorInfo = Tracer.ErrorInfo(args)
self.errors.append(errorInfo)
Report.info("Tracer caught Error: %s" % errorInfo.message)
self.has_errors = True
return False
def _on_assert(self, args):
assertInfo = Tracer.AssertInfo(args)
self.asserts.append(assertInfo)
@@ -436,6 +462,7 @@ class AngleHelper:
def vector3_str(vector3):
return "(x: {:.2f}, y: {:.2f}, z: {:.2f})".format(vector3.x, vector3.y, vector3.z)
def aabb_str(aabb):
return "[Min: %s, Max: %s]" % (vector3_str(aabb.min), vector3_str(aabb.max))
@@ -109,7 +109,7 @@
},
"MaterialLibrary": {
"assetId": {
"guid": "{62446378-67F8-5E49-AC31-761DD5942695}"
"guid": "{7CDF49C3-91A2-5C4E-B642-6D1AEC80E70E}"
},
"loadBehavior": "QueueLoad",
"assetHint": "assets/physics/surfacetypemateriallibrary.physmaterial"
@@ -121,7 +121,7 @@
},
"MaterialLibrary": {
"assetId": {
"guid": "{62446378-67F8-5E49-AC31-761DD5942695}"
"guid": "{7CDF49C3-91A2-5C4E-B642-6D1AEC80E70E}"
},
"loadBehavior": "QueueLoad",
"assetHint": "assets/physics/surfacetypemateriallibrary.physmaterial"
@@ -121,7 +121,7 @@
},
"MaterialLibrary": {
"assetId": {
"guid": "{62446378-67F8-5E49-AC31-761DD5942695}"
"guid": "{7CDF49C3-91A2-5C4E-B642-6D1AEC80E70E}"
},
"loadBehavior": "QueueLoad",
"assetHint": "assets/physics/surfacetypemateriallibrary.physmaterial"
@@ -121,7 +121,7 @@
},
"MaterialLibrary": {
"assetId": {
"guid": "{62446378-67F8-5E49-AC31-761DD5942695}"
"guid": "{7CDF49C3-91A2-5C4E-B642-6D1AEC80E70E}"
},
"loadBehavior": "QueueLoad",
"assetHint": "assets/physics/surfacetypemateriallibrary.physmaterial"
@@ -121,7 +121,7 @@
},
"MaterialLibrary": {
"assetId": {
"guid": "{62446378-67F8-5E49-AC31-761DD5942695}"
"guid": "{7CDF49C3-91A2-5C4E-B642-6D1AEC80E70E}"
},
"loadBehavior": "QueueLoad",
"assetHint": "assets/physics/surfacetypemateriallibrary.physmaterial"
@@ -103,7 +103,7 @@
},
"MaterialLibrary": {
"assetId": {
"guid": "{62446378-67F8-5E49-AC31-761DD5942695}"
"guid": "{7CDF49C3-91A2-5C4E-B642-6D1AEC80E70E}"
},
"loadBehavior": "QueueLoad",
"assetHint": "assets/physics/surfacetypemateriallibrary.physmaterial"
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:827a63985273229050bf4f63030bcc666f045091fe81cf8157a9ca23b40074b6
size 3214
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8d24963e6e8765205bc79cbe2304fc39f1245ee75249e2834a71c96c3cab824
size 22700
@@ -0,0 +1,8 @@
{
"values": [
{
"$type": "ScriptProcessorRule",
"scriptFilename": "Editor/Scripts/scene_mesh_to_prefab.py"
}
]
}
@@ -95,6 +95,7 @@ namespace SandboxEditor
cameras.AddCamera(m_firstPersonPanCamera);
cameras.AddCamera(m_firstPersonTranslateCamera);
cameras.AddCamera(m_firstPersonScrollCamera);
cameras.AddCamera(m_firstPersonFocusCamera);
cameras.AddCamera(m_pivotCamera);
});
@@ -111,6 +112,7 @@ namespace SandboxEditor
viewportId, &AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Events::BeginCursorCapture);
}
};
const auto showCursor = [viewportId = m_viewportId]
{
if (SandboxEditor::CameraCaptureCursorForLook())
@@ -172,19 +174,34 @@ namespace SandboxEditor
return SandboxEditor::CameraScrollSpeed();
};
const auto pivotFn = []
{
// use the manipulator transform as the pivot point
AZStd::optional<AZ::Transform> entityPivot;
AzToolsFramework::EditorTransformComponentSelectionRequestBus::EventResult(
entityPivot, AzToolsFramework::GetEntityContextId(),
&AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::GetManipulatorTransform);
if (entityPivot.has_value())
{
return entityPivot->GetTranslation();
}
// otherwise just use the identity
return AZ::Vector3::CreateZero();
};
m_firstPersonFocusCamera =
AZStd::make_shared<AzFramework::FocusCameraInput>(SandboxEditor::CameraFocusChannelId(), AzFramework::FocusLook);
m_firstPersonFocusCamera->SetPivotFn(pivotFn);
m_pivotCamera = AZStd::make_shared<AzFramework::PivotCameraInput>(SandboxEditor::CameraPivotChannelId());
m_pivotCamera->SetPivotFn(
[]([[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
[pivotFn]([[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
{
// use the manipulator transform as the pivot point
AZStd::optional<AZ::Transform> entityPivot;
AzToolsFramework::EditorTransformComponentSelectionRequestBus::EventResult(
entityPivot, AzToolsFramework::GetEntityContextId(),
&AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::GetManipulatorTransform);
// otherwise just use the identity
return entityPivot.value_or(AZ::Transform::CreateIdentity()).GetTranslation();
return pivotFn();
});
m_pivotRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraPivotLookChannelId());
@@ -244,11 +261,17 @@ namespace SandboxEditor
return SandboxEditor::CameraPanInvertedY();
};
m_pivotFocusCamera =
AZStd::make_shared<AzFramework::FocusCameraInput>(SandboxEditor::CameraFocusChannelId(), AzFramework::FocusPivot);
m_pivotFocusCamera->SetPivotFn(pivotFn);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotRotateCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotTranslateCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotDollyScrollCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotDollyMoveCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotPanCamera);
m_pivotCamera->m_pivotCameras.AddCamera(m_pivotFocusCamera);
}
void EditorModularViewportCameraComposer::OnEditorModularViewportCameraComposerSettingsChanged()
@@ -257,12 +280,14 @@ namespace SandboxEditor
m_firstPersonTranslateCamera->SetTranslateCameraInputChannelIds(translateCameraInputChannelIds);
m_firstPersonPanCamera->SetPanInputChannelId(SandboxEditor::CameraFreePanChannelId());
m_firstPersonRotateCamera->SetRotateInputChannelId(SandboxEditor::CameraFreeLookChannelId());
m_firstPersonFocusCamera->SetFocusInputChannelId(SandboxEditor::CameraFocusChannelId());
m_pivotCamera->SetPivotInputChannelId(SandboxEditor::CameraPivotChannelId());
m_pivotTranslateCamera->SetTranslateCameraInputChannelIds(translateCameraInputChannelIds);
m_pivotPanCamera->SetPanInputChannelId(SandboxEditor::CameraPivotPanChannelId());
m_pivotRotateCamera->SetRotateInputChannelId(SandboxEditor::CameraPivotLookChannelId());
m_pivotDollyMoveCamera->SetDollyInputChannelId(SandboxEditor::CameraPivotDollyChannelId());
m_pivotFocusCamera->SetFocusInputChannelId(SandboxEditor::CameraFocusChannelId());
}
void EditorModularViewportCameraComposer::OnViewportViewEntityChanged(const AZ::EntityId& viewEntityId)
@@ -42,12 +42,14 @@ namespace SandboxEditor
AZStd::shared_ptr<AzFramework::PanCameraInput> m_firstPersonPanCamera;
AZStd::shared_ptr<AzFramework::TranslateCameraInput> m_firstPersonTranslateCamera;
AZStd::shared_ptr<AzFramework::ScrollTranslationCameraInput> m_firstPersonScrollCamera;
AZStd::shared_ptr<AzFramework::FocusCameraInput> m_firstPersonFocusCamera;
AZStd::shared_ptr<AzFramework::PivotCameraInput> m_pivotCamera;
AZStd::shared_ptr<AzFramework::RotateCameraInput> m_pivotRotateCamera;
AZStd::shared_ptr<AzFramework::TranslateCameraInput> m_pivotTranslateCamera;
AZStd::shared_ptr<AzFramework::PivotDollyScrollCameraInput> m_pivotDollyScrollCamera;
AZStd::shared_ptr<AzFramework::PivotDollyMotionCameraInput> m_pivotDollyMoveCamera;
AZStd::shared_ptr<AzFramework::PanCameraInput> m_pivotPanCamera;
AZStd::shared_ptr<AzFramework::FocusCameraInput> m_pivotFocusCamera;
AzFramework::ViewportId m_viewportId;
};
@@ -91,7 +91,8 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Field("FreePan", &CameraInputSettings::m_freePanChannelId)
->Field("PivotLook", &CameraInputSettings::m_pivotLookChannelId)
->Field("PivotDolly", &CameraInputSettings::m_pivotDollyChannelId)
->Field("PivotPan", &CameraInputSettings::m_pivotPanChannelId);
->Field("PivotPan", &CameraInputSettings::m_pivotPanChannelId)
->Field("Focus", &CameraInputSettings::m_focusChannelId);
serialize.Class<CEditorPreferencesPage_ViewportCamera>()
->Version(1)
@@ -206,14 +207,17 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_pivotPanChannelId, "Pivot Pan",
"Key/button to begin camera pivot pan")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames)
->DataElement(
AZ::Edit::UIHandlers::ComboBox, &CameraInputSettings::m_focusChannelId, "Focus", "Key/button to focus camera pivot")
->Attribute(AZ::Edit::Attributes::StringList, &GetEditorInputNames);
editContext->Class<CEditorPreferencesPage_ViewportCamera>("Viewport Preferences", "Viewport Preferences")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ_CRC("PropertyVisibility_ShowChildrenOnly", 0xef428f20))
->DataElement(
AZ::Edit::UIHandlers::Default, &CEditorPreferencesPage_ViewportCamera::m_cameraMovementSettings,
"Camera Movement Settings", "Camera Movement Settings")
AZ::Edit::UIHandlers::Default, &CEditorPreferencesPage_ViewportCamera::m_cameraMovementSettings, "Camera Movement Settings",
"Camera Movement Settings")
->DataElement(
AZ::Edit::UIHandlers::Default, &CEditorPreferencesPage_ViewportCamera::m_cameraInputSettings, "Camera Input Settings",
"Camera Input Settings");
@@ -281,6 +285,7 @@ void CEditorPreferencesPage_ViewportCamera::OnApply()
SandboxEditor::SetCameraPivotLookChannelId(m_cameraInputSettings.m_pivotLookChannelId);
SandboxEditor::SetCameraPivotDollyChannelId(m_cameraInputSettings.m_pivotDollyChannelId);
SandboxEditor::SetCameraPivotPanChannelId(m_cameraInputSettings.m_pivotPanChannelId);
SandboxEditor::SetCameraFocusChannelId(m_cameraInputSettings.m_focusChannelId);
SandboxEditor::EditorModularViewportCameraComposerNotificationBus::Broadcast(
&SandboxEditor::EditorModularViewportCameraComposerNotificationBus::Events::OnEditorModularViewportCameraComposerSettingsChanged);
@@ -316,4 +321,5 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings()
m_cameraInputSettings.m_pivotLookChannelId = SandboxEditor::CameraPivotLookChannelId().GetName();
m_cameraInputSettings.m_pivotDollyChannelId = SandboxEditor::CameraPivotDollyChannelId().GetName();
m_cameraInputSettings.m_pivotPanChannelId = SandboxEditor::CameraPivotPanChannelId().GetName();
m_cameraInputSettings.m_focusChannelId = SandboxEditor::CameraFocusChannelId().GetName();
}
@@ -86,6 +86,7 @@ private:
AZStd::string m_pivotLookChannelId;
AZStd::string m_pivotDollyChannelId;
AZStd::string m_pivotPanChannelId;
AZStd::string m_focusChannelId;
};
CameraMovementSettings m_cameraMovementSettings;
+11
View File
@@ -50,6 +50,7 @@ namespace SandboxEditor
constexpr AZStd::string_view CameraPivotLookIdSetting = "/Amazon/Preferences/Editor/Camera/PivotLookId";
constexpr AZStd::string_view CameraPivotDollyIdSetting = "/Amazon/Preferences/Editor/Camera/PivotDollyId";
constexpr AZStd::string_view CameraPivotPanIdSetting = "/Amazon/Preferences/Editor/Camera/PivotPanId";
constexpr AZStd::string_view CameraFocusIdSetting = "/Amazon/Preferences/Editor/Camera/FocusId";
template<typename T>
void SetRegistry(const AZStd::string_view setting, T&& value)
@@ -462,4 +463,14 @@ namespace SandboxEditor
{
SetRegistry(CameraPivotPanIdSetting, cameraPivotPanId);
}
AzFramework::InputChannelId CameraFocusChannelId()
{
return AzFramework::InputChannelId(GetRegistry(CameraFocusIdSetting, AZStd::string("keyboard_key_alphanumeric_X")).c_str());
}
void SetCameraFocusChannelId(AZStd::string_view cameraFocusId)
{
SetRegistry(CameraFocusIdSetting, cameraFocusId);
}
} // namespace SandboxEditor
+3
View File
@@ -136,4 +136,7 @@ namespace SandboxEditor
SANDBOX_API AzFramework::InputChannelId CameraPivotPanChannelId();
SANDBOX_API void SetCameraPivotPanChannelId(AZStd::string_view cameraPivotPanId);
SANDBOX_API AzFramework::InputChannelId CameraFocusChannelId();
SANDBOX_API void SetCameraFocusChannelId(AZStd::string_view cameraFocusId);
} // namespace SandboxEditor
+3 -9
View File
@@ -135,18 +135,12 @@ namespace AZ
{
stringLength = strlen(uuidString);
}
if (stringLength > MaxPermissiveStringSize)
{
if (!skipWarnings)
{
AZ_Warning("Math", false, "Can't create UUID from string length %zu over maximum %zu", stringLength, MaxPermissiveStringSize);
}
return Uuid::CreateNull();
}
size_t newLength{ 0 };
char createString[MaxPermissiveStringSize];
for (size_t curPos = 0; curPos < stringLength; ++curPos)
// Loop until we get to the end of the string OR stop once we've accumulated a full UUID string worth of data
for (size_t curPos = 0; curPos < stringLength && newLength < ValidUuidStringLength; ++curPos)
{
char curChar = uuidString[curPos];
switch (curChar)
+2 -1
View File
@@ -42,8 +42,9 @@ namespace AZ
//VER_AZ_RANDOM_CRC32 = 6, // 0 1 1 0
};
static constexpr int ValidUuidStringLength = 32; /// Number of characters (data only, no extra formatting) in a valid UUID string
static const size_t MaxStringBuffer = 39; /// 32 Uuid + 4 dashes + 2 brackets + 1 terminate
Uuid() {}
Uuid(const char* string, size_t stringLength = 0) { *this = CreateString(string, stringLength); }
+23
View File
@@ -247,4 +247,27 @@ namespace UnitTest
Uuid right = Uuid::CreateStringPermissive(permissiveStr1);
EXPECT_EQ(left, right);
}
TEST_F(UuidTests, CreateStringPermissive_StringWithExtraData_Succeeds)
{
const char uuidStr[] = "{34D44249-E599-4B30-811F-4215C2DEA269}";
Uuid left = Uuid::CreateString(uuidStr);
const char permissiveStr[] = "0x34D44249-0xE5994B30-0x811F4215-0xC2DEA269 Hello World";
Uuid right = Uuid::CreateStringPermissive(permissiveStr);
EXPECT_EQ(left, right);
}
TEST_F(UuidTests, CreateStringPermissive_StringWithLotsOfExtraData_Succeeds)
{
const char uuidStr[] = "{34D44249-E599-4B30-811F-4215C2DEA269}";
Uuid left = Uuid::CreateString(uuidStr);
const char permissiveStr[] = "0x34D44249-0xE5994B30-0x811F4215-0xC2DEA269 Hello World this is a really long string "
"with lots of extra data to make sure we can parse a long string without failing as long as the uuid is in "
"the beginning of the string then we should succeed";
Uuid right = Uuid::CreateStringPermissive(permissiveStr);
EXPECT_EQ(left, right);
}
}
@@ -133,6 +133,8 @@ namespace AzFramework
behaviorContext->Class<BehaviorEntity>("Entity")
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
->Attribute(AZ::Script::Attributes::ConstructorOverride, &Internal::BehaviorEntityScriptConstructor)
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "entity")
->Constructor()
->Constructor<AZ::EntityId>()
->Constructor<AZ::Entity*>()
@@ -623,15 +623,24 @@ namespace AzFramework
{
Camera nextCamera = targetCamera;
const auto pivotDirection = targetCamera.m_offset.GetNormalized();
nextCamera.m_offset -= pivotDirection * delta;
const auto pivotDot = targetCamera.m_offset.Dot(nextCamera.m_offset);
const auto distance = nextCamera.m_offset.GetLength() * AZ::GetSign(pivotDot);
const auto minDistance = 0.01f;
if (distance < minDistance || pivotDot < 0.0f)
// handle case where pivot and offset may be the same to begin with
// choose negative y-axis for offset to default to moving the camera backwards from the pivot (standard centered pivot behavior)
const auto pivotDirection = [&targetCamera]
{
nextCamera.m_offset = pivotDirection * minDistance;
if (const auto offsetLength = targetCamera.m_offset.GetLength(); AZ::IsCloseMag(offsetLength, 0.0f))
{
return -AZ::Vector3::CreateAxisY();
}
else
{
return targetCamera.m_offset / offsetLength;
}
}();
nextCamera.m_offset -= pivotDirection * delta;
if (pivotDirection.Dot(nextCamera.m_offset) < 0.0f)
{
nextCamera.m_offset = pivotDirection * 0.001f;
}
return nextCamera;
@@ -771,6 +780,73 @@ namespace AzFramework
return camera;
}
FocusCameraInput::FocusCameraInput(const InputChannelId& focusChannelId, FocusOffsetFn offsetFn)
: m_focusChannelId(focusChannelId)
, m_offsetFn(offsetFn)
{
}
bool FocusCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
if (const auto* input = AZStd::get_if<DiscreteInputEvent>(&event))
{
if (input->m_channelId == m_focusChannelId && input->m_state == InputChannel::State::Began)
{
BeginActivation();
}
}
return !Idle();
}
Camera FocusCameraInput::StepCamera(
const Camera& targetCamera,
[[maybe_unused]] const ScreenVector& cursorDelta,
[[maybe_unused]] float scrollDelta,
[[maybe_unused]] float deltaTime)
{
if (Beginning())
{
// as the camera starts, record the camera we would like to end up as
m_nextCamera.m_offset = m_offsetFn(m_pivotFn().GetDistance(targetCamera.Translation()));
const auto angles =
EulerAngles(AZ::Matrix3x3::CreateFromMatrix3x4(AZ::Matrix3x4::CreateLookAt(targetCamera.Translation(), m_pivotFn())));
m_nextCamera.m_pitch = angles.GetX();
m_nextCamera.m_yaw = angles.GetZ();
m_nextCamera.m_pivot = targetCamera.m_pivot;
}
// end the behavior when the camera is in alignment
if (AZ::IsCloseMag(targetCamera.m_pitch, m_nextCamera.m_pitch) && AZ::IsCloseMag(targetCamera.m_yaw, m_nextCamera.m_yaw))
{
EndActivation();
}
return m_nextCamera;
}
void FocusCameraInput::SetPivotFn(PivotFn pivotFn)
{
m_pivotFn = AZStd::move(pivotFn);
}
void FocusCameraInput::SetFocusInputChannelId(const InputChannelId& focusChannelId)
{
m_focusChannelId = focusChannelId;
}
bool CustomCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, const float scrollDelta)
{
return m_handleEventsFn(*this, event, cursorDelta, scrollDelta);
}
Camera CustomCameraInput::StepCamera(
const Camera& targetCamera, const ScreenVector& cursorDelta, const float scrollDelta, const float deltaTime)
{
return m_stepCameraFn(*this, targetCamera, cursorDelta, scrollDelta, deltaTime);
}
InputEvent BuildInputEvent(const InputChannel& inputChannel, const WindowSize& windowSize)
{
const auto& inputChannelId = inputChannel.GetInputChannelId();
@@ -617,6 +617,60 @@ namespace AzFramework
return true;
}
//! Callback to use for FocusCameraInput when a free look camera is being used.
//! @note This is when offset is zero.
inline AZ::Vector3 FocusLook(float)
{
return AZ::Vector3::CreateZero();
}
//! Callback to use for FocusCameraInput when a pivot camera is being used.
//! @note This is when offset is non zero.
inline AZ::Vector3 FocusPivot(const float length)
{
return AZ::Vector3::CreateAxisY(-length);
}
using FocusOffsetFn = AZStd::function<AZ::Vector3(float)>;
//! A focus behavior to align the camera view to the position returned by the pivot function.
//! @note This only alters the camera orientation, the translation is unaffected.
class FocusCameraInput : public CameraInput
{
public:
using PivotFn = AZStd::function<AZ::Vector3()>;
FocusCameraInput(const InputChannelId& focusChannelId, FocusOffsetFn offsetFn);
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
//! Override the default behavior for how a pivot point is calculated.
void SetPivotFn(PivotFn pivotFn);
void SetFocusInputChannelId(const InputChannelId& focusChannelId);
private:
InputChannelId m_focusChannelId; //!< Input channel to begin the focus camera input.
Camera m_nextCamera;
PivotFn m_pivotFn;
FocusOffsetFn m_offsetFn;
};
//! Provides a CameraInput type that can be implemented without needing to create a new type deriving from CameraInput.
//! This can be very useful for specific use cases that are less generally applicable.
class CustomCameraInput : public CameraInput
{
public:
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
AZStd::function<bool(CameraInput&, const InputEvent&, const ScreenVector&, float)> m_handleEventsFn;
AZStd::function<Camera(CameraInput&, const Camera&, const ScreenVector&, float, float)> m_stepCameraFn;
};
//! Map from a generic InputChannel event to a camera specific InputEvent.
InputEvent BuildInputEvent(const InputChannel& inputChannel, const WindowSize& windowSize);
} // namespace AzFramework
@@ -69,6 +69,7 @@
#include <AzToolsFramework/UI/EditorEntityUi/EditorEntityUiSystemComponent.h>
#include <AzToolsFramework/Undo/UndoCacheInterface.h>
#include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
#include <Entity/EntityUtilityComponent.h>
#include <QtWidgets/QMessageBox>
AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") // 4251: 'QFileInfo::d_ptr': class 'QSharedDataPointer<QFileInfoPrivate>' needs to have dll-interface to be used by clients of class 'QFileInfo'
@@ -271,7 +272,8 @@ namespace AzToolsFramework
azrtti_typeid<AzToolsFramework::EditorInteractionSystemComponent>(),
azrtti_typeid<Components::EditorEntitySearchComponent>(),
azrtti_typeid<Components::EditorIntersectorComponent>(),
azrtti_typeid<AzToolsFramework::SliceRequestComponent>()
azrtti_typeid<AzToolsFramework::SliceRequestComponent>(),
azrtti_typeid<AzToolsFramework::EntityUtilityComponent>()
});
return components;
@@ -53,6 +53,7 @@
#include <AzToolsFramework/Thumbnails/ThumbnailerNullComponent.h>
#include <AzToolsFramework/AssetBrowser/AssetBrowserComponent.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemComponent.h>
#include <AzToolsFramework/Entity/EntityUtilityComponent.h>
AZ_DEFINE_BUDGET(AzToolsFramework);
@@ -71,6 +72,7 @@ namespace AzToolsFramework
Components::EditorSelectionAccentSystemComponent::CreateDescriptor(),
EditorEntityContextComponent::CreateDescriptor(),
EditorEntityFixupComponent::CreateDescriptor(),
EntityUtilityComponent::CreateDescriptor(),
ContainerEntitySystemComponent::CreateDescriptor(),
FocusModeSystemComponent::CreateDescriptor(),
SliceMetadataEntityContextComponent::CreateDescriptor(),
@@ -0,0 +1,351 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <sstream>
#include <AzCore/JSON/rapidjson.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/JsonSerializationSettings.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzFramework/Entity/EntityContext.h>
#include <AzFramework/FileFunc/FileFunc.h>
#include <AzToolsFramework/Entity/EntityUtilityComponent.h>
#include <Entity/EditorEntityContextBus.h>
#include <rapidjson/document.h>
namespace AzToolsFramework
{
void ComponentDetails::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<ComponentDetails>()
->Field("TypeInfo", &ComponentDetails::m_typeInfo)
->Field("BaseClasses", &ComponentDetails::m_baseClasses);
serializeContext->RegisterGenericType<AZStd::vector<ComponentDetails>>();
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<ComponentDetails>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "entity")
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All)
->Property("TypeInfo", BehaviorValueProperty(&ComponentDetails::m_typeInfo))
->Property("BaseClasses", BehaviorValueProperty(&ComponentDetails::m_baseClasses))
->Method("__repr__", [](const ComponentDetails& obj)
{
std::ostringstream result;
bool first = true;
for (const auto& baseClass : obj.m_baseClasses)
{
if (!first)
{
result << ", ";
}
first = false;
result << baseClass.c_str();
}
return AZStd::string::format("%s, Base Classes: <%s>", obj.m_typeInfo.c_str(), result.str().c_str());
})
->Attribute(AZ::Script::Attributes::Operator, AZ::Script::Attributes::OperatorType::ToString);
}
}
AZ::EntityId EntityUtilityComponent::CreateEditorReadyEntity(const AZStd::string& entityName)
{
auto* newEntity = m_entityContext->CreateEntity(entityName.c_str());
if (!newEntity)
{
AZ_Error("EditorEntityUtility", false, "Failed to create new entity %s", entityName.c_str());
return AZ::EntityId();
}
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
&AzToolsFramework::EditorEntityContextRequestBus::Events::AddRequiredComponents, *newEntity);
newEntity->Init();
auto newEntityId = newEntity->GetId();
m_createdEntities.emplace_back(newEntityId);
return newEntityId;
}
AZ::TypeId GetComponentTypeIdFromName(const AZStd::string& typeName)
{
// Try to create a TypeId first. We won't show any warnings if this fails as the input might be a class name instead
AZ::TypeId typeId = AZ::TypeId::CreateStringPermissive(typeName.data());
// If the typeId is null, try a lookup by class name
if (typeId.IsNull())
{
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
auto typeNameCrc = AZ::Crc32(typeName.data());
auto typeUuidList = serializeContext->FindClassId(typeNameCrc);
// TypeId is invalid or class name is invalid
if (typeUuidList.empty())
{
AZ_Error("EntityUtilityComponent", false, "Provided type %s is either an invalid TypeId or does not match any class names", typeName.c_str());
return AZ::TypeId::CreateNull();
}
typeId = typeUuidList[0];
}
return typeId;
}
AZ::Component* FindComponentHelper(AZ::EntityId entityId, const AZ::TypeId& typeId, AZ::ComponentId componentId, bool createComponent = false)
{
AZ::Entity* entity = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, entityId);
if (!entity)
{
AZ_Error("EntityUtilityComponent", false, "Invalid entityId %s", entityId.ToString().c_str());
return nullptr;
}
AZ::Component* component = nullptr;
if (componentId != AZ::InvalidComponentId)
{
component = entity->FindComponent(componentId);
}
else
{
component = entity->FindComponent(typeId);
}
if (!component && createComponent)
{
component = entity->CreateComponent(typeId);
}
if (!component)
{
AZ_Error(
"EntityUtilityComponent", false, "Failed to find component (%s) on entity %s (%s)",
componentId != AZ::InvalidComponentId ? AZStd::to_string(componentId).c_str()
: typeId.ToString<AZStd::string>().c_str(),
entityId.ToString().c_str(),
entity->GetName().c_str());
return nullptr;
}
return component;
}
AzFramework::BehaviorComponentId EntityUtilityComponent::GetOrAddComponentByTypeName(AZ::EntityId entityId, const AZStd::string& typeName)
{
AZ::TypeId typeId = GetComponentTypeIdFromName(typeName);
if (typeId.IsNull())
{
return AzFramework::BehaviorComponentId(AZ::InvalidComponentId);
}
AZ::Component* component = FindComponentHelper(entityId, typeId, AZ::InvalidComponentId, true);
return component ? AzFramework::BehaviorComponentId(component->GetId()) :
AzFramework::BehaviorComponentId(AZ::InvalidComponentId);
}
bool EntityUtilityComponent::UpdateComponentForEntity(AZ::EntityId entityId, AzFramework::BehaviorComponentId componentId, const AZStd::string& json)
{
if (!componentId.IsValid())
{
AZ_Error("EntityUtilityComponent", false, "Invalid componentId passed to UpdateComponentForEntity");
return false;
}
AZ::Component* component = FindComponentHelper(entityId, AZ::TypeId::CreateNull(), componentId);
if (!component)
{
return false;
}
using namespace AZ::JsonSerializationResult;
AZ::JsonDeserializerSettings settings = AZ::JsonDeserializerSettings{};
settings.m_reporting = []([[maybe_unused]] AZStd::string_view message, ResultCode result, AZStd::string_view) -> auto
{
if (result.GetProcessing() == Processing::Halted)
{
AZ_Error("EntityUtilityComponent", false, "JSON %s\n", message.data());
}
else if (result.GetOutcome() > Outcomes::PartialDefaults)
{
AZ_Warning("EntityUtilityComponent", false, "JSON %s\n", message.data());
}
return result;
};
rapidjson::Document doc;
doc.Parse<rapidjson::kParseCommentsFlag>(json.data(), json.size());
ResultCode resultCode = AZ::JsonSerialization::Load(*component, doc, settings);
return resultCode.GetProcessing() != Processing::Halted;
}
AZStd::string EntityUtilityComponent::GetComponentDefaultJson(const AZStd::string& typeName)
{
AZ::TypeId typeId = GetComponentTypeIdFromName(typeName);
if (typeId.IsNull())
{
// GetComponentTypeIdFromName already does error handling
return "";
}
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
const AZ::SerializeContext::ClassData* classData = serializeContext->FindClassData(typeId);
if (!classData)
{
AZ_Error("EntityUtilityComponent", false, "Failed to find ClassData for typeId %s (%s)", typeId.ToString<AZStd::string>().c_str(), typeName.c_str());
return "";
}
void* component = classData->m_factory->Create("Component");
rapidjson::Document document;
AZ::JsonSerializerSettings settings;
settings.m_keepDefaults = true;
auto resultCode = AZ::JsonSerialization::Store(document, document.GetAllocator(), component, nullptr, typeId, settings);
// Clean up the allocated component ASAP, we don't need it anymore
classData->m_factory->Destroy(component);
if (resultCode.GetProcessing() == AZ::JsonSerializationResult::Processing::Halted)
{
AZ_Error("EntityUtilityComponent", false, "Failed to serialize component to json (%s): %s",
typeName.c_str(), resultCode.ToString(typeName).c_str())
return "";
}
AZStd::string jsonString;
AZ::Outcome<void, AZStd::string> outcome = AZ::JsonSerializationUtils::WriteJsonString(document, jsonString);
if (!outcome.IsSuccess())
{
AZ_Error("EntityUtilityComponent", false, "Failed to write component json to string: %s", outcome.GetError().c_str());
return "";
}
return jsonString;
}
AZStd::vector<ComponentDetails> EntityUtilityComponent::FindMatchingComponents(const AZStd::string& searchTerm)
{
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
if (m_typeInfo.empty())
{
serializeContext->EnumerateDerived<AZ::Component>(
[this, serializeContext](const AZ::SerializeContext::ClassData* classData, const AZ::Uuid& /*typeId*/)
{
auto& typeInfo = m_typeInfo.emplace_back(classData->m_typeId, classData->m_name, AZStd::vector<AZStd::string>{});
serializeContext->EnumerateBase(
[&typeInfo](const AZ::SerializeContext::ClassData* classData, const AZ::Uuid&)
{
if (classData)
{
AZStd::get<2>(typeInfo).emplace_back(classData->m_name);
}
return true;
},
classData->m_typeId);
return true;
});
}
AZStd::vector<ComponentDetails> matches;
for (const auto& [typeId, typeName, baseClasses] : m_typeInfo)
{
if (AZStd::wildcard_match(searchTerm, typeName))
{
ComponentDetails details;
details.m_typeInfo = AZStd::string::format("%s %s", typeId.ToString<AZStd::string>().c_str(), typeName.c_str());
details.m_baseClasses = baseClasses;
matches.emplace_back(AZStd::move(details));
}
}
return matches;
}
void EntityUtilityComponent::ResetEntityContext()
{
for (AZ::EntityId entityId : m_createdEntities)
{
m_entityContext->DestroyEntityById(entityId);
}
m_createdEntities.clear();
m_entityContext->ResetContext();
}
void EntityUtilityComponent::Reflect(AZ::ReflectContext* context)
{
ComponentDetails::Reflect(context);
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<EntityUtilityComponent, AZ::Component>();
}
if (auto* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty("InvalidComponentId", BehaviorConstant(AZ::InvalidComponentId))
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Category, "Entity")
->Attribute(AZ::Script::Attributes::Module, "entity");
behaviorContext->EBus<EntityUtilityBus>("EntityUtilityBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All)
->Attribute(AZ::Script::Attributes::Category, "Entity")
->Attribute(AZ::Script::Attributes::Module, "entity")
->Event("CreateEditorReadyEntity", &EntityUtilityBus::Events::CreateEditorReadyEntity)
->Event("GetOrAddComponentByTypeName", &EntityUtilityBus::Events::GetOrAddComponentByTypeName)
->Event("UpdateComponentForEntity", &EntityUtilityBus::Events::UpdateComponentForEntity)
->Event("FindMatchingComponents", &EntityUtilityBus::Events::FindMatchingComponents)
->Event("GetComponentDefaultJson", &EntityUtilityBus::Events::GetComponentDefaultJson)
;
}
}
void EntityUtilityComponent::Activate()
{
m_entityContext = AZStd::make_unique<AzFramework::EntityContext>(UtilityEntityContextId);
m_entityContext->InitContext();
EntityUtilityBus::Handler::BusConnect();
}
void EntityUtilityComponent::Deactivate()
{
EntityUtilityBus::Handler::BusDisconnect();
m_entityContext = nullptr;
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Component/Component.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
#include <AzToolsFramework/ToolsComponents/EditorDisabledCompositionBus.h>
#include <AzToolsFramework/ToolsComponents/EditorPendingCompositionBus.h>
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
#include <AzCore/Component/ComponentApplication.h>
#include <AzFramework/Entity/BehaviorEntity.h>
namespace AzToolsFramework
{
struct ComponentDetails
{
AZ_TYPE_INFO(AzToolsFramework::ComponentDetails, "{107D8379-4AD4-4547-BEE1-184B120F23E9}");
static void Reflect(AZ::ReflectContext* context);
AZStd::string m_typeInfo;
AZStd::vector<AZStd::string> m_baseClasses;
};
// This ebus is intended to provide behavior-context friendly APIs to create and manage entities
struct EntityUtilityTraits : AZ::EBusTraits
{
AZ_RTTI(AzToolsFramework::EntityUtilityTraits, "{A6305CAE-C825-43F9-A44D-E503910912AF}");
virtual ~EntityUtilityTraits() = default;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
// Creates an entity with the default editor components attached and initializes the entity
virtual AZ::EntityId CreateEditorReadyEntity(const AZStd::string& entityName) = 0;
virtual AzFramework::BehaviorComponentId GetOrAddComponentByTypeName(AZ::EntityId entity, const AZStd::string& typeName) = 0;
virtual bool UpdateComponentForEntity(AZ::EntityId entity, AzFramework::BehaviorComponentId component, const AZStd::string& json) = 0;
// Gets a JSON string containing describing the default serialization state of the specified component
virtual AZStd::string GetComponentDefaultJson(const AZStd::string& typeName) = 0;
// Returns a list of matching component type names. Supports wildcard search terms
virtual AZStd::vector<ComponentDetails> FindMatchingComponents(const AZStd::string& searchTerm) = 0;
virtual void ResetEntityContext() = 0;
};
using EntityUtilityBus = AZ::EBus<EntityUtilityTraits>;
struct EntityUtilityComponent : AZ::Component
, EntityUtilityBus::Handler
{
inline const static AZ::Uuid UtilityEntityContextId = AZ::Uuid("{9C277B88-E79E-4F8A-BAFF-A4C175BD565F}");
AZ_COMPONENT(EntityUtilityComponent, "{47205907-A0EA-4FFF-A620-04D20C04A379}");
AZ::EntityId CreateEditorReadyEntity(const AZStd::string& entityName) override;
AzFramework::BehaviorComponentId GetOrAddComponentByTypeName(AZ::EntityId entity, const AZStd::string& typeName) override;
bool UpdateComponentForEntity(AZ::EntityId entity, AzFramework::BehaviorComponentId component, const AZStd::string& json) override;
AZStd::string GetComponentDefaultJson(const AZStd::string& typeName) override;
AZStd::vector<ComponentDetails> FindMatchingComponents(const AZStd::string& searchTerm) override;
void ResetEntityContext() override;
static void Reflect(AZ::ReflectContext* context);
protected:
void Activate() override;
void Deactivate() override;
// Our own entity context. This API is intended mostly for use in Asset Builders where there is no editor context
// Additionally, an entity context is needed when using the Behavior Entity class
AZStd::unique_ptr<AzFramework::EntityContext> m_entityContext;
// TypeId, TypeName, Vector<BaseClassName>
AZStd::vector<AZStd::tuple<AZ::TypeId, AZStd::string, AZStd::vector<AZStd::string>>> m_typeInfo;
// Keep track of the entities we create so they can be reset
AZStd::vector<AZ::EntityId> m_createdEntities;
};
}; // namespace AzToolsFramework
@@ -10,6 +10,7 @@
#include <AzCore/Component/EntityId.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Entity/EntityContextBus.h>
@@ -50,17 +50,19 @@ namespace AzToolsFramework
auto settingsRegistry = AZ::SettingsRegistry::Get();
AZ_Assert(settingsRegistry, "Settings registry is not set");
[[maybe_unused]] bool result =
settingsRegistry->Get(m_projectPathWithOsSeparator.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath);
AZ_Warning("Prefab", result, "Couldn't retrieve project root path");
m_projectPathWithSlashSeparator = AZ::IO::Path(m_projectPathWithOsSeparator.Native(), '/').MakePreferred();
AZ::Interface<PrefabLoaderInterface>::Register(this);
m_scriptingPrefabLoader.Connect(this);
}
void PrefabLoader::UnregisterPrefabLoaderInterface()
{
m_scriptingPrefabLoader.Disconnect();
AZ::Interface<PrefabLoaderInterface>::Unregister(this);
}
@@ -568,7 +570,7 @@ namespace AzToolsFramework
(pathStr.find_first_of(AZ_FILESYSTEM_INVALID_CHARACTERS) == AZStd::string::npos) &&
(pathStr.back() != '\\' && pathStr.back() != '/');
}
AZ::IO::Path PrefabLoader::GetFullPath(AZ::IO::PathView path)
{
AZ::IO::Path pathWithOSSeparator = AZ::IO::Path(path).MakePreferred();
@@ -596,26 +598,38 @@ namespace AzToolsFramework
{
// The asset system provided us with a valid root folder and relative path, so return it.
fullPath = AZ::IO::Path(rootFolder) / assetInfo.m_relativePath;
return fullPath;
}
else
{
// If for some reason the Asset system couldn't provide a relative path, provide some fallback logic.
// Check to see if the AssetProcessor is ready. If it *is* and we didn't get a path, print an error then follow
// the fallback logic. If it's *not* ready, we're probably either extremely early in a tool startup flow or inside
// a unit test, so just execute the fallback logic without an error.
[[maybe_unused]] bool assetProcessorReady = false;
AzFramework::AssetSystemRequestBus::BroadcastResult(
assetProcessorReady, &AzFramework::AssetSystemRequestBus::Events::AssetProcessorIsReady);
AZ_Error(
"Prefab", !assetProcessorReady, "Full source path for '%.*s' could not be determined. Using fallback logic.",
AZ_STRING_ARG(path.Native()));
// If a relative path was passed in, make it relative to the project root.
fullPath = AZ::IO::Path(m_projectPathWithOsSeparator).Append(pathWithOSSeparator);
// attempt to find the absolute from the Cache folder
AZStd::string assetRootFolder;
if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
{
settingsRegistry->Get(assetRootFolder, AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder);
}
fullPath = AZ::IO::Path(assetRootFolder) / path;
if (fullPath.IsAbsolute() && AZ::IO::SystemFile::Exists(fullPath.c_str()))
{
return fullPath;
}
}
// If for some reason the Asset system couldn't provide a relative path, provide some fallback logic.
// Check to see if the AssetProcessor is ready. If it *is* and we didn't get a path, print an error then follow
// the fallback logic. If it's *not* ready, we're probably either extremely early in a tool startup flow or inside
// a unit test, so just execute the fallback logic without an error.
[[maybe_unused]] bool assetProcessorReady = false;
AzFramework::AssetSystemRequestBus::BroadcastResult(
assetProcessorReady, &AzFramework::AssetSystemRequestBus::Events::AssetProcessorIsReady);
AZ_Error(
"Prefab", !assetProcessorReady, "Full source path for '%.*s' could not be determined. Using fallback logic.",
AZ_STRING_ARG(path.Native()));
// If a relative path was passed in, make it relative to the project root.
fullPath = AZ::IO::Path(m_projectPathWithOsSeparator).Append(pathWithOSSeparator);
return fullPath;
}
@@ -15,6 +15,7 @@
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/string/string.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
#include <Prefab/ScriptingPrefabLoader.h>
namespace AZ
{
@@ -114,6 +115,7 @@ namespace AzToolsFramework
void SetSaveAllPrefabsPreference(SaveAllPrefabsPreference saveAllPrefabsPreference) override;
private:
/**
* Copies the template dom provided and manipulates it into the proper format to be saved to disk.
* @param templateRef The template whose dom we want to transform into the proper format to be saved to disk.
@@ -177,6 +179,7 @@ namespace AzToolsFramework
AZStd::optional<AZStd::pair<PrefabDom, AZ::IO::Path>> StoreTemplateIntoFileFormat(TemplateId templateId);
PrefabSystemComponentInterface* m_prefabSystemComponentInterface = nullptr;
ScriptingPrefabLoader m_scriptingPrefabLoader;
AZ::IO::Path m_projectPathWithOsSeparator;
AZ::IO::Path m_projectPathWithSlashSeparator;
};
@@ -99,7 +99,6 @@ namespace AzToolsFramework
// Generates a new path
static AZ::IO::Path GeneratePath();
};
} // namespace Prefab
} // namespace AzToolsFramework
@@ -0,0 +1,45 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Interface/Interface.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/Prefab/PrefabIdTypes.h>
#include <AzCore/EBus/EBus.h>
namespace AzToolsFramework
{
namespace Prefab
{
// Ebus for script-friendly APIs for the prefab loader
struct PrefabLoaderScriptingTraits : AZ::EBusTraits
{
AZ_TYPE_INFO(PrefabLoaderScriptingTraits, "{C344B7D8-8299-48C9-8450-26E1332EA011}");
virtual ~PrefabLoaderScriptingTraits() = default;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
/**
* Saves a Prefab Template into the provided output string.
* Converts Prefab Template form into .prefab form by collapsing nested Template info
* into a source path and patches.
* @param templateId Id of the template to be saved
* @return Will contain the serialized template json on success
*/
virtual AZ::Outcome<AZStd::string, void> SaveTemplateToString(TemplateId templateId) = 0;
};
using PrefabLoaderScriptingBus = AZ::EBus<PrefabLoaderScriptingTraits>;
} // namespace Prefab
} // namespace AzToolsFramework
@@ -10,6 +10,7 @@
#include <AzCore/Component/Entity.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityIdMapper.h>
@@ -36,12 +37,14 @@ namespace AzToolsFramework
m_instanceToTemplatePropagator.RegisterInstanceToTemplateInterface();
m_prefabPublicHandler.RegisterPrefabPublicHandlerInterface();
m_prefabPublicRequestHandler.Connect();
m_prefabSystemScriptingHandler.Connect(this);
AZ::SystemTickBus::Handler::BusConnect();
}
void PrefabSystemComponent::Deactivate()
{
AZ::SystemTickBus::Handler::BusDisconnect();
m_prefabSystemScriptingHandler.Disconnect();
m_prefabPublicRequestHandler.Disconnect();
m_prefabPublicHandler.UnregisterPrefabPublicHandlerInterface();
m_instanceToTemplatePropagator.UnregisterInstanceToTemplateInterface();
@@ -58,13 +61,24 @@ namespace AzToolsFramework
AzToolsFramework::Prefab::PrefabConversionUtils::EditorInfoRemover::Reflect(context);
PrefabPublicRequestHandler::Reflect(context);
PrefabLoader::Reflect(context);
PrefabSystemScriptingHandler::Reflect(context);
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
if (serialize)
if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
{
serialize->Class<PrefabSystemComponent, AZ::Component>()->Version(1);
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<PrefabLoaderScriptingBus>("PrefabLoaderScriptingBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "prefab")
->Attribute(AZ::Script::Attributes::Category, "Prefab")
->Event("SaveTemplateToString", &PrefabLoaderScriptingBus::Events::SaveTemplateToString);
;
}
AZ::JsonRegistrationContext* jsonRegistration = azrtti_cast<AZ::JsonRegistrationContext*>(context);
if (jsonRegistration)
{
@@ -145,7 +159,7 @@ namespace AzToolsFramework
newInstance->SetTemplateId(newTemplateId);
}
}
void PrefabSystemComponent::PropagateTemplateChanges(TemplateId templateId, bool immediate, InstanceOptionalReference instanceToExclude)
{
UpdatePrefabInstances(templateId, immediate, instanceToExclude);
@@ -27,6 +27,7 @@
#include <AzToolsFramework/Prefab/PrefabPublicRequestHandler.h>
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
#include <Prefab/PrefabSystemScriptingHandler.h>
namespace AZ
{
@@ -219,7 +220,7 @@ namespace AzToolsFramework
const AZStd::vector<AZ::Entity*>& entities, AZStd::vector<AZStd::unique_ptr<Instance>>&& instancesToConsume,
AZ::IO::PathView filePath, AZStd::unique_ptr<AZ::Entity> containerEntity = nullptr,
InstanceOptionalReference parent = AZStd::nullopt, bool shouldCreateLinks = true) override;
PrefabDom& FindTemplateDom(TemplateId templateId) override;
/**
@@ -244,7 +245,7 @@ namespace AzToolsFramework
private:
AZ_DISABLE_COPY_MOVE(PrefabSystemComponent);
/**
* Builds a new Prefab Template out of entities and instances and returns the first instance comprised of
* these entities and instances.
@@ -412,6 +413,8 @@ namespace AzToolsFramework
// Handler of the public Prefab requests.
PrefabPublicRequestHandler m_prefabPublicRequestHandler;
PrefabSystemScriptingHandler m_prefabSystemScriptingHandler;
};
} // namespace Prefab
} // namespace AzToolsFramework
@@ -78,8 +78,7 @@ namespace AzToolsFramework
AZStd::unique_ptr<AZ::Entity> containerEntity = nullptr, InstanceOptionalReference parent = AZStd::nullopt,
bool shouldCreateLinks = true) = 0;
};
} // namespace Prefab
} // namespace AzToolsFramework
@@ -0,0 +1,38 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Interface/Interface.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/Link/Link.h>
#include <AzToolsFramework/Prefab/PrefabIdTypes.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/EBus/EBus.h>
namespace AzToolsFramework
{
namespace Prefab
{
// Bus that exposes a script-friendly interface to the PrefabSystemComponent
struct PrefabSystemScriptingEbusTraits : AZ::EBusTraits
{
using MutexType = AZ::NullMutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
virtual TemplateId CreatePrefabTemplate(
const AZStd::vector<AZ::EntityId>& entityIds, const AZStd::string& filePath) = 0;
};
using PrefabSystemScriptingBus = AZ::EBus<PrefabSystemScriptingEbusTraits>;
} // namespace Prefab
} // namespace AzToolsFramework
@@ -0,0 +1,75 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <Prefab/PrefabSystemComponentInterface.h>
#include <Prefab/PrefabSystemScriptingHandler.h>
#include <AzCore/Component/Entity.h>
namespace AzToolsFramework::Prefab
{
void PrefabSystemScriptingHandler::Reflect(AZ::ReflectContext* context)
{
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty("InvalidTemplateId", BehaviorConstant(InvalidTemplateId))
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "prefab")
->Attribute(AZ::Script::Attributes::Category, "Prefab");
behaviorContext->EBus<PrefabSystemScriptingBus>("PrefabSystemScriptingBus")
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "prefab")
->Attribute(AZ::Script::Attributes::Category, "Prefab")
->Event("CreatePrefab", &PrefabSystemScriptingBus::Events::CreatePrefabTemplate);
}
}
void PrefabSystemScriptingHandler::Connect(PrefabSystemComponentInterface* prefabSystemComponentInterface)
{
AZ_Assert(prefabSystemComponentInterface != nullptr, "prefabSystemComponentInterface must not be null");
m_prefabSystemComponentInterface = prefabSystemComponentInterface;
PrefabSystemScriptingBus::Handler::BusConnect();
}
void PrefabSystemScriptingHandler::Disconnect()
{
PrefabSystemScriptingBus::Handler::BusDisconnect();
}
TemplateId PrefabSystemScriptingHandler::CreatePrefabTemplate(const AZStd::vector<AZ::EntityId>& entityIds, const AZStd::string& filePath)
{
AZStd::vector<AZ::Entity*> entities;
for (const auto& entityId : entityIds)
{
AZ::Entity* entity = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, entityId);
AZ_Warning(
"PrefabSystemComponent", entity, "EntityId %s was not found and will not be added to the prefab",
entityId.ToString().c_str());
if (entity)
{
entities.push_back(entity);
}
}
auto prefab = m_prefabSystemComponentInterface->CreatePrefab(entities, {}, AZ::IO::PathView(AZStd::string_view(filePath)));
if (!prefab)
{
AZ_Error("PrefabSystemComponenent", false, "Failed to create prefab %s", filePath.c_str());
return InvalidTemplateId;
}
return prefab->GetTemplateId();
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <Prefab/PrefabSystemScriptingBus.h>
namespace AzToolsFramework
{
namespace Prefab
{
class PrefabSystemScriptingHandler
: PrefabSystemScriptingBus::Handler
{
public:
static void Reflect(AZ::ReflectContext* context);
PrefabSystemScriptingHandler() = default;
void Connect(PrefabSystemComponentInterface* prefabSystemComponentInterface);
void Disconnect();
private:
AZ_DISABLE_COPY(PrefabSystemScriptingHandler);
//////////////////////////////////////////////////////////////////////////
// PrefabSystemScriptingBus implementation
TemplateId CreatePrefabTemplate(const AZStd::vector<AZ::EntityId>& entityIds, const AZStd::string& filePath) override;
//////////////////////////////////////////////////////////////////////////
PrefabSystemComponentInterface* m_prefabSystemComponentInterface = nullptr;
};
} // namespace Prefab
} // namespace AzToolsFramework
@@ -0,0 +1,144 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzFramework/FileFunc/FileFunc.h>
namespace AZ::Prefab
{
static constexpr const char s_useProceduralPrefabsKey[] = "/O3DE/Preferences/Prefabs/UseProceduralPrefabs";
// ProceduralPrefabAsset
ProceduralPrefabAsset::ProceduralPrefabAsset(const AZ::Data::AssetId& assetId)
: AZ::Data::AssetData(assetId)
, m_templateId(AzToolsFramework::Prefab::InvalidTemplateId)
{
}
void ProceduralPrefabAsset::Reflect(AZ::ReflectContext* context)
{
PrefabDomData::Reflect(context);
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<ProceduralPrefabAsset, AZ::Data::AssetData>()
->Version(1)
->Field("Template Name", &ProceduralPrefabAsset::m_templateName)
->Field("Template ID", &ProceduralPrefabAsset::m_templateId);
}
}
const AZStd::string& ProceduralPrefabAsset::GetTemplateName() const
{
return m_templateName;
}
void ProceduralPrefabAsset::SetTemplateName(AZStd::string templateName)
{
m_templateName = AZStd::move(templateName);
}
AzToolsFramework::Prefab::TemplateId ProceduralPrefabAsset::GetTemplateId() const
{
return m_templateId;
}
void ProceduralPrefabAsset::SetTemplateId(AzToolsFramework::Prefab::TemplateId templateId)
{
m_templateId = templateId;
}
bool ProceduralPrefabAsset::UseProceduralPrefabs()
{
bool useProceduralPrefabs = false;
bool result = AZ::SettingsRegistry::Get()->GetObject(useProceduralPrefabs, s_useProceduralPrefabsKey);
return result && useProceduralPrefabs;
}
// PrefabDomData
void PrefabDomData::Reflect(AZ::ReflectContext* context)
{
if (auto* jsonContext = azrtti_cast<AZ::JsonRegistrationContext*>(context))
{
jsonContext->Serializer<PrefabDomDataJsonSerializer>()->HandlesType<PrefabDomData>();
}
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<PrefabDomData>()
->Version(1);
}
}
void PrefabDomData::CopyValue(const rapidjson::Value& inputValue)
{
m_prefabDom.CopyFrom(inputValue, m_prefabDom.GetAllocator());
}
const AzToolsFramework::Prefab::PrefabDom& PrefabDomData::GetValue() const
{
return m_prefabDom;
}
// PrefabDomDataJsonSerializer
AZ::JsonSerializationResult::Result PrefabDomDataJsonSerializer::Load(
void* outputValue,
[[maybe_unused]] const AZ::Uuid& outputValueTypeId,
const rapidjson::Value& inputValue,
AZ::JsonDeserializerContext& context)
{
AZ_Assert(outputValueTypeId == azrtti_typeid<PrefabDomData>(),
"PrefabDomDataJsonSerializer Load against output typeID that was not PrefabDomData");
AZ_Assert(outputValue, "PrefabDomDataJsonSerializer Load against null output");
namespace JSR = AZ::JsonSerializationResult;
JSR::ResultCode result(JSR::Tasks::ReadField);
if (inputValue.IsObject() == false)
{
result.Combine(context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Missing, "Missing object"));
return context.Report(result, "Prefab should be an object.");
}
if (inputValue.MemberCount() < 1)
{
result.Combine(context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Missing, "Missing members"));
return context.Report(result, "Prefab should have multiple members.");
}
auto* outputVariable = reinterpret_cast<PrefabDomData*>(outputValue);
outputVariable->CopyValue(inputValue);
return context.Report(result, "Loaded procedural prefab");
}
AZ::JsonSerializationResult::Result PrefabDomDataJsonSerializer::Store(
rapidjson::Value& outputValue,
const void* inputValue,
[[maybe_unused]] const void* defaultValue,
[[maybe_unused]] const AZ::Uuid& valueTypeId,
AZ::JsonSerializerContext& context)
{
AZ_Assert(inputValue, "Input value for PrefabDomDataJsonSerializer can't be null.");
AZ_Assert(azrtti_typeid<PrefabDomData>() == valueTypeId,
"Unable to Serialize because the provided type is not PrefabGroup::PrefabDomData.");
const PrefabDomData* prefabDomData = reinterpret_cast<const PrefabDomData*>(inputValue);
namespace JSR = AZ::JsonSerializationResult;
JSR::ResultCode result(JSR::Tasks::WriteValue);
outputValue.SetObject();
outputValue.CopyFrom(prefabDomData->GetValue(), context.GetJsonAllocator());
return context.Report(result, "Stored procedural prefab");
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Asset/AssetCommon.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
#include <AzToolsFramework/Prefab/PrefabIdTypes.h>
#include <AzCore/Serialization/Json/BaseJsonSerializer.h>
namespace AZ::Prefab
{
//! A wrapper around the JSON DOM type so that the assets can read in and write out
//! JSON directly since Prefabs are JSON serialized entity-component data
class PrefabDomData final
{
public:
AZ_RTTI(PrefabDomData, "{C73A3360-D772-4D41-9118-A039BF9340C1}");
AZ_CLASS_ALLOCATOR(PrefabDomData, AZ::SystemAllocator, 0);
PrefabDomData() = default;
~PrefabDomData() = default;
static void Reflect(AZ::ReflectContext* context);
void CopyValue(const rapidjson::Value& inputValue);
const AzToolsFramework::Prefab::PrefabDom& GetValue() const;
private:
AzToolsFramework::Prefab::PrefabDom m_prefabDom;
};
//! Registered to help read/write JSON for the PrefabDomData::m_prefabDom
class PrefabDomDataJsonSerializer final
: public AZ::BaseJsonSerializer
{
public:
AZ_RTTI(PrefabDomDataJsonSerializer, "{9FC48652-A00B-4EFA-8FD9-345A8E625439}", BaseJsonSerializer);
AZ_CLASS_ALLOCATOR(PrefabDomDataJsonSerializer, AZ::SystemAllocator, 0);
~PrefabDomDataJsonSerializer() override = default;
AZ::JsonSerializationResult::Result Load(
void* outputValue,
const AZ::Uuid& outputValueTypeId,
const rapidjson::Value& inputValue,
AZ::JsonDeserializerContext& context) override;
AZ::JsonSerializationResult::Result Store(
rapidjson::Value& outputValue,
const void* inputValue,
const void* defaultValue,
const AZ::Uuid& valueTypeId,
AZ::JsonSerializerContext& context) override;
};
//! An asset type to register templates into the Prefab system so that they
//! can instantiate like Authored Prefabs
class ProceduralPrefabAsset
: public AZ::Data::AssetData
{
public:
AZ_CLASS_ALLOCATOR(ProceduralPrefabAsset, AZ::SystemAllocator, 0);
AZ_RTTI(ProceduralPrefabAsset, "{9B7C8459-471E-4EAD-A363-7990CC4065A9}", AZ::Data::AssetData);
static bool UseProceduralPrefabs();
ProceduralPrefabAsset(const AZ::Data::AssetId& assetId = AZ::Data::AssetId());
~ProceduralPrefabAsset() override = default;
ProceduralPrefabAsset(const ProceduralPrefabAsset& rhs) = delete;
ProceduralPrefabAsset& operator=(const ProceduralPrefabAsset& rhs) = delete;
const AZStd::string& GetTemplateName() const;
void SetTemplateName(AZStd::string templateName);
AzToolsFramework::Prefab::TemplateId GetTemplateId() const;
void SetTemplateId(AzToolsFramework::Prefab::TemplateId templateId);
static void Reflect(AZ::ReflectContext* context);
private:
AZStd::string m_templateName;
AzToolsFramework::Prefab::TemplateId m_templateId;
};
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Prefab/ScriptingPrefabLoader.h>
namespace AzToolsFramework::Prefab
{
void ScriptingPrefabLoader::Connect(PrefabLoaderInterface* prefabLoaderInterface)
{
AZ_Assert(prefabLoaderInterface, "prefabLoaderInterface must not be null");
m_prefabLoaderInterface = prefabLoaderInterface;
PrefabLoaderScriptingBus::Handler::BusConnect();
}
void ScriptingPrefabLoader::Disconnect()
{
PrefabLoaderScriptingBus::Handler::BusDisconnect();
}
AZ::Outcome<AZStd::string, void> ScriptingPrefabLoader::SaveTemplateToString(TemplateId templateId)
{
AZStd::string json;
if (m_prefabLoaderInterface->SaveTemplateToString(templateId, json))
{
return AZ::Success(json);
}
return AZ::Failure();
}
} // namespace AzToolsFramework::Prefab
@@ -0,0 +1,42 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <Prefab/PrefabLoaderInterface.h>
#include <Prefab/PrefabLoaderScriptingBus.h>
namespace AzToolsFramework
{
namespace Prefab
{
/**
* The Scripting Prefab Loader handles scripting-friendly API requests for the prefab loader
*/
class ScriptingPrefabLoader
: private PrefabLoaderScriptingBus::Handler
{
public:
AZ_CLASS_ALLOCATOR(ScriptingPrefabLoader, AZ::SystemAllocator, 0);
AZ_RTTI(ScriptingPrefabLoader, "{ABC3C989-4D4F-41E7-B25B-B0FEF97177E6}");
void Connect(PrefabLoaderInterface* prefabLoaderInterface);
void Disconnect();
private:
//////////////////////////////////////////////////////////////////////////
// PrefabLoaderRequestBus implementation
AZ::Outcome<AZStd::string, void> SaveTemplateToString(TemplateId templateId) override;
//////////////////////////////////////////////////////////////////////////
PrefabLoaderInterface* m_prefabLoaderInterface = nullptr;
};
} // namespace Prefab
} // namespace AzToolsFramework
@@ -13,6 +13,7 @@
#include <AzCore/StringFunc/StringFunc.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzFramework/Asset/AssetSystemBus.h>
@@ -25,6 +26,9 @@
#include <AzToolsFramework/ContainerEntity/ContainerEntityInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabLoaderInterface.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/ToolsComponents/EditorLayerComponentBus.h>
#include <AzToolsFramework/UI/EditorEntityUi/EditorEntityUiInterface.h>
#include <AzToolsFramework/UI/Prefab/PrefabIntegrationInterface.h>
@@ -218,6 +222,16 @@ namespace AzToolsFramework
instantiateAction, &QAction::triggered, instantiateAction, [] { ContextMenu_InstantiatePrefab(); });
}
// Instantiate Procedural Prefab
if (AZ::Prefab::ProceduralPrefabAsset::UseProceduralPrefabs())
{
QAction* action = menu->addAction(QObject::tr("Instantiate Procedural Prefab..."));
action->setToolTip(QObject::tr("Instantiates a procedural prefab file in a prefab."));
QObject::connect(
action, &QAction::triggered, action, [] { ContextMenu_InstantiateProceduralPrefab(); });
}
menu->addSeparator();
bool itemWasShown = false;
@@ -435,6 +449,38 @@ namespace AzToolsFramework
}
}
void PrefabIntegrationManager::ContextMenu_InstantiateProceduralPrefab()
{
AZStd::string prefabAssetPath;
bool hasUserForProceduralPrefabAsset = QueryUserForProceduralPrefabAsset(prefabAssetPath);
if (hasUserForProceduralPrefabAsset)
{
AZ::EntityId parentId;
AZ::Vector3 position = AZ::Vector3::CreateZero();
EntityIdList selectedEntities;
ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities);
if (selectedEntities.size() == 1)
{
parentId = selectedEntities.front();
AZ::TransformBus::EventResult(position, parentId, &AZ::TransformInterface::GetWorldTranslation);
}
else
{
// otherwise return since it needs to be inside an authored prefab
return;
}
// Instantiating from context menu always puts the instance at the root level
auto createPrefabOutcome = s_prefabPublicInterface->InstantiatePrefab(prefabAssetPath, parentId, position);
if (!createPrefabOutcome.IsSuccess())
{
WarnUserOfError("Prefab Instantiation Error", createPrefabOutcome.GetError());
}
}
}
void PrefabIntegrationManager::ContextMenu_EditPrefab(AZ::EntityId containerEntity)
{
s_prefabFocusInterface->FocusOnOwningPrefab(containerEntity);
@@ -690,6 +736,33 @@ namespace AzToolsFramework
return true;
}
bool PrefabIntegrationManager::QueryUserForProceduralPrefabAsset(AZStd::string& outPrefabAssetPath)
{
using namespace AzToolsFramework;
auto selection = AssetBrowser::AssetSelectionModel::AssetTypeSelection(azrtti_typeid<AZ::Prefab::ProceduralPrefabAsset>());
EditorRequests::Bus::Broadcast(&AzToolsFramework::EditorRequests::BrowseForAssets, selection);
if (!selection.IsValid())
{
return false;
}
auto product = azrtti_cast<const ProductAssetBrowserEntry*>(selection.GetResult());
if (product == nullptr)
{
return false;
}
outPrefabAssetPath = product->GetRelativePath();
auto asset = AZ::Data::AssetManager::Instance().GetAsset(
product->GetAssetId(),
azrtti_typeid<AZ::Prefab::ProceduralPrefabAsset>(),
AZ::Data::AssetLoadBehavior::Default);
return asset.BlockUntilLoadComplete() != AZ::Data::AssetData::AssetStatus::Error;
}
void PrefabIntegrationManager::WarnUserOfError(AZStd::string_view title, AZStd::string_view message)
{
QWidget* activeWindow = QApplication::activeWindow();
@@ -91,6 +91,7 @@ namespace AzToolsFramework
// Context menu item handlers
static void ContextMenu_CreatePrefab(AzToolsFramework::EntityIdList selectedEntities);
static void ContextMenu_InstantiatePrefab();
static void ContextMenu_InstantiateProceduralPrefab();
static void ContextMenu_EditPrefab(AZ::EntityId containerEntity);
static void ContextMenu_SavePrefab(AZ::EntityId containerEntity);
static void ContextMenu_DeleteSelected();
@@ -101,6 +102,7 @@ namespace AzToolsFramework
const AZStd::string& suggestedName, const char* initialTargetDirectory, AZ::u32 prefabUserSettingsId, QWidget* activeWindow,
AZStd::string& outPrefabName, AZStd::string& outPrefabFilePath);
static bool QueryUserForPrefabFilePath(AZStd::string& outPrefabFilePath);
static bool QueryUserForProceduralPrefabAsset(AZStd::string& outPrefabAssetPath);
static void WarnUserOfError(AZStd::string_view title, AZStd::string_view message);
// Path and filename generation
@@ -152,6 +152,8 @@ set(FILES
Entity/SliceEditorEntityOwnershipService.h
Entity/SliceEditorEntityOwnershipService.cpp
Entity/SliceEditorEntityOwnershipServiceBus.h
Entity/EntityUtilityComponent.h
Entity/EntityUtilityComponent.cpp
Fingerprinting/TypeFingerprinter.h
Fingerprinting/TypeFingerprinter.cpp
FocusMode/FocusModeInterface.h
@@ -646,9 +648,15 @@ set(FILES
Prefab/PrefabLoader.h
Prefab/PrefabLoader.cpp
Prefab/PrefabLoaderInterface.h
Prefab/PrefabLoaderScriptingBus.h
Prefab/ScriptingPrefabLoader.h
Prefab/ScriptingPrefabLoader.cpp
Prefab/PrefabSystemComponent.h
Prefab/PrefabSystemComponent.cpp
Prefab/PrefabSystemComponentInterface.h
Prefab/PrefabSystemScriptingBus.h
Prefab/PrefabSystemScriptingHandler.h
Prefab/PrefabSystemScriptingHandler.cpp
Prefab/Instance/Instance.h
Prefab/Instance/Instance.cpp
Prefab/Instance/InstanceSerializer.h
@@ -671,6 +679,8 @@ set(FILES
Prefab/Instance/TemplateInstanceMapperInterface.h
Prefab/Link/Link.h
Prefab/Link/Link.cpp
Prefab/Procedural/ProceduralPrefabAsset.h
Prefab/Procedural/ProceduralPrefabAsset.cpp
Prefab/PrefabPublicHandler.h
Prefab/PrefabPublicHandler.cpp
Prefab/PrefabPublicInterface.h
@@ -0,0 +1,252 @@
/*
* 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 <AzFramework/Entity/BehaviorEntity.h>
#include <AzTest/AzTest.h>
#include <AzToolsFramework/Application/ToolsApplication.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <Entity/EntityUtilityComponent.h>
#include <ToolsComponents/TransformComponent.h>
namespace UnitTest
{
// Global variables for communicating between Lua test code and C++
AZ::EntityId g_globalEntityId = AZ::EntityId{};
AZStd::string g_globalString = "";
AzFramework::BehaviorComponentId g_globalComponentId = {};
AZStd::vector<AzToolsFramework::ComponentDetails> g_globalComponentDetails = {};
bool g_globalBool = false;
class EntityUtilityComponentTests
: public ToolsApplicationFixture
{
void InitProperties()
{
AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface<AZ::ComponentApplicationRequests>::Get();
ASSERT_NE(componentApplicationRequests, nullptr);
auto behaviorContext = componentApplicationRequests->GetBehaviorContext();
ASSERT_NE(behaviorContext, nullptr);
behaviorContext->Property("g_globalEntityId", BehaviorValueProperty(&g_globalEntityId));
behaviorContext->Property("g_globalString", BehaviorValueProperty(&g_globalString));
behaviorContext->Property("g_globalComponentId", BehaviorValueProperty(&g_globalComponentId));
behaviorContext->Property("g_globalBool", BehaviorValueProperty(&g_globalBool));
behaviorContext->Property("g_globalComponentDetails", BehaviorValueProperty(&g_globalComponentDetails));
g_globalEntityId = AZ::EntityId{};
g_globalString = AZStd::string{};
g_globalComponentId = AzFramework::BehaviorComponentId{};
g_globalBool = false;
g_globalComponentDetails = AZStd::vector<AzToolsFramework::ComponentDetails>{};
}
void SetUpEditorFixtureImpl() override
{
InitProperties();
}
void TearDownEditorFixtureImpl() override
{
g_globalString.set_capacity(0); // Free all memory
g_globalComponentDetails.set_capacity(0);
}
};
TEST_F(EntityUtilityComponentTests, CreateEntity)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
g_globalEntityId = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
my_entity = Entity(g_globalEntityId)
g_globalString = my_entity:GetName()
)LUA");
EXPECT_NE(g_globalEntityId, AZ::EntityId{});
EXPECT_STREQ(g_globalString.c_str(), "test");
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(g_globalEntityId);
ASSERT_NE(entity, nullptr);
// Test cleaning up, make sure the entity is destroyed
AzToolsFramework::EntityUtilityBus::Broadcast(&AzToolsFramework::EntityUtilityBus::Events::ResetEntityContext);
entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(g_globalEntityId);
ASSERT_EQ(entity, nullptr);
}
TEST_F(EntityUtilityComponentTests, CreateEntityEmptyName)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
g_globalEntityId = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("")
)LUA");
EXPECT_NE(g_globalEntityId, AZ::EntityId{});
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(g_globalEntityId);
ASSERT_NE(entity, nullptr);
}
TEST_F(EntityUtilityComponentTests, FindComponent)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
ent_id = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
g_globalComponentId = EntityUtilityBus.Broadcast.GetOrAddComponentByTypeName(ent_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0 TransformComponent")
)LUA");
EXPECT_TRUE(g_globalComponentId.IsValid());
}
TEST_F(EntityUtilityComponentTests, InvalidComponentName)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
AZ_TEST_START_TRACE_SUPPRESSION;
sc.Execute(R"LUA(
ent_id = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
g_globalComponentId = EntityUtilityBus.Broadcast.GetOrAddComponentByTypeName(ent_id, "ThisIsNotAComponent-Error")
)LUA");
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
EXPECT_FALSE(g_globalComponentId.IsValid());
}
TEST_F(EntityUtilityComponentTests, InvalidComponentId)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
AZ_TEST_START_TRACE_SUPPRESSION;
sc.Execute(R"LUA(
ent_id = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
g_globalComponentId = EntityUtilityBus.Broadcast.GetOrAddComponentByTypeName(ent_id, "{1234-hello-world-this-is-not-an-id}")
)LUA");
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // Should get 1 error stating the type id is not valid
EXPECT_FALSE(g_globalComponentId.IsValid());
}
TEST_F(EntityUtilityComponentTests, CreateComponent)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
ent_id = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
g_globalComponentId = EntityUtilityBus.Broadcast.GetOrAddComponentByTypeName(ent_id, "ScriptEditorComponent")
)LUA");
EXPECT_TRUE(g_globalComponentId.IsValid());
}
TEST_F(EntityUtilityComponentTests, UpdateComponent)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
g_globalEntityId = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
comp_id = EntityUtilityBus.Broadcast.GetOrAddComponentByTypeName(g_globalEntityId, "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent")
json_update = [[
{
"Transform Data": { "Rotate": [0.0, 0.1, 180.0] }
}
]]
g_globalBool = EntityUtilityBus.Broadcast.UpdateComponentForEntity(g_globalEntityId, comp_id, json_update);
)LUA");
EXPECT_TRUE(g_globalBool);
EXPECT_NE(g_globalEntityId, AZ::EntityId(AZ::EntityId::InvalidEntityId));
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(g_globalEntityId);
auto* transformComponent = entity->FindComponent<AzToolsFramework::Components::TransformComponent>();
ASSERT_NE(transformComponent, nullptr);
AZ::Vector3 localRotation = transformComponent->GetLocalRotationQuaternion().GetEulerDegrees();
EXPECT_EQ(localRotation, AZ::Vector3(.0f, 0.1f, 180.0f));
}
TEST_F(EntityUtilityComponentTests, GetComponentJson)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
g_globalString = EntityUtilityBus.Broadcast.GetComponentDefaultJson("ScriptEditorComponent")
)LUA");
EXPECT_STRNE(g_globalString.c_str(), "");
}
TEST_F(EntityUtilityComponentTests, GetComponentJsonDoesNotExist)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
AZ_TEST_START_TRACE_SUPPRESSION;
sc.Execute(R"LUA(
g_globalString = EntityUtilityBus.Broadcast.GetComponentDefaultJson("404")
)LUA");
AZ_TEST_STOP_TRACE_SUPPRESSION(1); // 1 error: Failed to find component id for type name 404
EXPECT_STREQ(g_globalString.c_str(), "");
}
TEST_F(EntityUtilityComponentTests, SearchComponents)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
g_globalComponentDetails = EntityUtilityBus.Broadcast.FindMatchingComponents("Transform*")
)LUA");
// There should be 2 transform components
EXPECT_EQ(g_globalComponentDetails.size(), 2);
}
TEST_F(EntityUtilityComponentTests, SearchComponentsNotFound)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
g_globalComponentDetails = EntityUtilityBus.Broadcast.FindMatchingComponents("404")
)LUA");
EXPECT_EQ(g_globalComponentDetails.size(), 0);
}
}
@@ -0,0 +1,173 @@
/*
* 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 <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <Entity/EntityUtilityComponent.h>
#include <Prefab/PrefabTestComponent.h>
#include <Prefab/PrefabTestDomUtils.h>
#include <Prefab/PrefabTestFixture.h>
namespace UnitTest
{
TemplateId g_globalTemplateId = {};
AZStd::string g_globalPrefabString = "";
class PrefabScriptingTest : public PrefabTestFixture
{
void InitProperties() const
{
AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface<AZ::ComponentApplicationRequests>::Get();
ASSERT_NE(componentApplicationRequests, nullptr);
auto behaviorContext = componentApplicationRequests->GetBehaviorContext();
ASSERT_NE(behaviorContext, nullptr);
behaviorContext->Property("g_globalTemplateId", BehaviorValueProperty(&g_globalTemplateId));
behaviorContext->Property("g_globalPrefabString", BehaviorValueProperty(&g_globalPrefabString));
g_globalTemplateId = TemplateId{};
g_globalPrefabString = AZStd::string{};
}
void SetUpEditorFixtureImpl() override
{
InitProperties();
}
void TearDownEditorFixtureImpl() override
{
g_globalPrefabString.set_capacity(0); // Free all memory
}
};
TEST_F(PrefabScriptingTest, PrefabScripting_CreatePrefab)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
my_id = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
entities = vector_EntityId()
entities:push_back(my_id)
g_globalTemplateId = PrefabSystemScriptingBus.Broadcast.CreatePrefab(entities, "test.prefab")
)LUA");
EXPECT_NE(g_globalTemplateId, TemplateId{});
auto prefabSystemComponentInterface = AZ::Interface<PrefabSystemComponentInterface>::Get();
ASSERT_NE(prefabSystemComponentInterface, nullptr);
TemplateReference templateRef = prefabSystemComponentInterface->FindTemplate(g_globalTemplateId);
EXPECT_TRUE(templateRef);
}
TEST_F(PrefabScriptingTest, PrefabScripting_CreatePrefab_NoEntities)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
my_id = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
entities = vector_EntityId()
g_globalTemplateId = PrefabSystemScriptingBus.Broadcast.CreatePrefab(entities, "test.prefab")
)LUA");
EXPECT_NE(g_globalTemplateId, TemplateId{});
auto prefabSystemComponentInterface = AZ::Interface<PrefabSystemComponentInterface>::Get();
ASSERT_NE(prefabSystemComponentInterface, nullptr);
TemplateReference templateRef = prefabSystemComponentInterface->FindTemplate(g_globalTemplateId);
EXPECT_TRUE(templateRef);
}
TEST_F(PrefabScriptingTest, PrefabScripting_CreatePrefab_NoPath)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
AZ_TEST_START_TRACE_SUPPRESSION;
sc.Execute(R"LUA(
my_id = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
entities = vector_EntityId()
template_id = PrefabSystemScriptingBus.Broadcast.CreatePrefab(entities, "")
)LUA");
/*
error: PrefabSystemComponent::CreateTemplateFromInstance - Attempted to create a prefab template from an instance without a source file path. Unable to proceed.
error: Failed to create a Template associated with file path during CreatePrefab.
error: Failed to create prefab
*/
AZ_TEST_STOP_TRACE_SUPPRESSION(3);
}
TEST_F(PrefabScriptingTest, PrefabScripting_SaveToString)
{
AZ::ScriptContext sc;
auto behaviorContext = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->GetBehaviorContext();
sc.BindTo(behaviorContext);
sc.Execute(R"LUA(
my_id = EntityUtilityBus.Broadcast.CreateEditorReadyEntity("test")
entities = vector_EntityId()
entities:push_back(my_id)
template_id = PrefabSystemScriptingBus.Broadcast.CreatePrefab(entities, "test.prefab")
my_result = PrefabLoaderScriptingBus.Broadcast.SaveTemplateToString(template_id)
if my_result:IsSuccess() then
g_globalPrefabString = my_result:GetValue()
end
)LUA");
auto prefabSystemComponentInterface = AZ::Interface<PrefabSystemComponentInterface>::Get();
prefabSystemComponentInterface->RemoveAllTemplates();
EXPECT_STRNE(g_globalPrefabString.c_str(), "");
TemplateId templateFromString = AZ::Interface<PrefabLoaderInterface>::Get()->LoadTemplateFromString(g_globalPrefabString);
EXPECT_NE(templateFromString, InvalidTemplateId);
// Create another entity for comparison purposes
AZ::EntityId entityId;
AzToolsFramework::EntityUtilityBus::BroadcastResult(
entityId, &AzToolsFramework::EntityUtilityBus::Events::CreateEditorReadyEntity, "test");
AZ::Entity* testEntity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(entityId);
// Instantiate the prefab we saved
AZStd::unique_ptr<Instance> instance = prefabSystemComponentInterface->InstantiatePrefab(templateFromString);
EXPECT_NE(instance, nullptr);
AZStd::vector<const AZ::Entity*> loadedEntities;
// Get the entities from the instance
instance->GetConstEntities(
[&loadedEntities](const AZ::Entity& entity)
{
loadedEntities.push_back(&entity);
return true;
});
// Make sure the instance has an entity with the same number of components as our test entity
EXPECT_EQ(loadedEntities.size(), 1);
EXPECT_EQ(loadedEntities[0]->GetComponents().size(), testEntity->GetComponents().size());
g_globalPrefabString.set_capacity(0); // Free all memory
}
}
@@ -0,0 +1,135 @@
/*
* 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 <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <Entity/EntityUtilityComponent.h>
#include <Prefab/PrefabTestComponent.h>
#include <Prefab/PrefabTestDomUtils.h>
#include <Prefab/PrefabTestFixture.h>
#include <Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
namespace UnitTest
{
class ProceduralPrefabAssetTest
: public PrefabTestFixture
{
void SetUpEditorFixtureImpl() override
{
AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface<AZ::ComponentApplicationRequests>::Get();
ASSERT_NE(componentApplicationRequests, nullptr);
auto* behaviorContext = componentApplicationRequests->GetBehaviorContext();
ASSERT_NE(behaviorContext, nullptr);
auto* jsonRegistrationContext = componentApplicationRequests->GetJsonRegistrationContext();
ASSERT_NE(jsonRegistrationContext, nullptr);
auto* serializeContext = componentApplicationRequests->GetSerializeContext();
ASSERT_NE(serializeContext, nullptr);
AZ::Prefab::ProceduralPrefabAsset::Reflect(serializeContext);
AZ::Prefab::ProceduralPrefabAsset::Reflect(behaviorContext);
AZ::Prefab::ProceduralPrefabAsset::Reflect(jsonRegistrationContext);
}
void TearDownEditorFixtureImpl() override
{
AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface<AZ::ComponentApplicationRequests>::Get();
componentApplicationRequests->GetJsonRegistrationContext()->EnableRemoveReflection();
AZ::Prefab::ProceduralPrefabAsset::Reflect(componentApplicationRequests->GetJsonRegistrationContext());
}
};
TEST_F(ProceduralPrefabAssetTest, ReflectContext_AccessMethods_Works)
{
AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface<AZ::ComponentApplicationRequests>::Get();
auto* serializeContext = componentApplicationRequests->GetSerializeContext();
EXPECT_TRUE(!serializeContext->CreateAny(azrtti_typeid<AZ::Prefab::ProceduralPrefabAsset>()).empty());
EXPECT_TRUE(!serializeContext->CreateAny(azrtti_typeid<AZ::Prefab::PrefabDomData>()).empty());
auto* jsonRegistrationContext = componentApplicationRequests->GetJsonRegistrationContext();
EXPECT_TRUE(jsonRegistrationContext->GetSerializerForSerializerType(azrtti_typeid<AZ::Prefab::PrefabDomDataJsonSerializer>()));
}
TEST_F(ProceduralPrefabAssetTest, ProceduralPrefabAsset_AccessMethods_Works)
{
const auto templateId = TemplateId(1);
const auto prefabString = "fake.prefab";
AZ::Prefab::ProceduralPrefabAsset asset{};
asset.SetTemplateId(templateId);
EXPECT_EQ(asset.GetTemplateId(), templateId);
asset.SetTemplateName(prefabString);
EXPECT_EQ(asset.GetTemplateName(), prefabString);
}
TEST_F(ProceduralPrefabAssetTest, PrefabDomData_AccessMethods_Works)
{
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
dom.AddMember("boolValue", true, dom.GetAllocator());
AZ::Prefab::PrefabDomData prefabDomData;
prefabDomData.CopyValue(dom);
const AzToolsFramework::Prefab::PrefabDom& result = prefabDomData.GetValue();
EXPECT_TRUE(result.HasMember("boolValue"));
EXPECT_TRUE(result.FindMember("boolValue")->value.GetBool());
}
TEST_F(ProceduralPrefabAssetTest, PrefabDomDataJsonSerializer_Load_Works)
{
AZ::Prefab::PrefabDomData prefabDomData;
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
dom.AddMember("member", "value", dom.GetAllocator());
AZ::Prefab::PrefabDomDataJsonSerializer prefabDomDataJsonSerializer;
AZ::JsonDeserializerSettings settings;
settings.m_reporting = [](auto, auto, auto)
{
AZ::JsonSerializationResult::ResultCode result(AZ::JsonSerializationResult::Tasks::ReadField);
return result;
};
AZ::JsonDeserializerContext context{ settings };
auto result = prefabDomDataJsonSerializer.Load(&prefabDomData, azrtti_typeid(prefabDomData), dom, context);
EXPECT_EQ(result.GetResultCode().GetOutcome(), AZ::JsonSerializationResult::Outcomes::DefaultsUsed);
EXPECT_TRUE(prefabDomData.GetValue().HasMember("member"));
EXPECT_STREQ(prefabDomData.GetValue().FindMember("member")->value.GetString(), "value");
}
TEST_F(ProceduralPrefabAssetTest, PrefabDomDataJsonSerializer_Store_Works)
{
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
dom.AddMember("member", "value", dom.GetAllocator());
AZ::Prefab::PrefabDomData prefabDomData;
prefabDomData.CopyValue(dom);
AZ::Prefab::PrefabDomDataJsonSerializer prefabDomDataJsonSerializer;
AzToolsFramework::Prefab::PrefabDom outputValue;
AZ::JsonSerializerSettings settings;
settings.m_reporting = [](auto, auto, auto)
{
AZ::JsonSerializationResult::ResultCode result(AZ::JsonSerializationResult::Tasks::WriteValue);
return result;
};
AZ::JsonSerializerContext context{ settings, outputValue.GetAllocator() };
auto result = prefabDomDataJsonSerializer.Store(outputValue, &prefabDomData, nullptr, azrtti_typeid(prefabDomData), context);
EXPECT_EQ(result.GetResultCode().GetOutcome(), AZ::JsonSerializationResult::Outcomes::DefaultsUsed);
EXPECT_TRUE(outputValue.HasMember("member"));
EXPECT_STREQ(outputValue.FindMember("member")->value.GetString(), "value");
}
}
@@ -27,6 +27,7 @@ set(FILES
Entity/EditorEntityHelpersTests.cpp
Entity/EditorEntitySearchComponentTests.cpp
Entity/EditorEntitySelectionTests.cpp
Entity/EntityUtilityComponentTests.cpp
EntityIdQLabelTests.cpp
EntityInspectorTests.cpp
EntityOwnershipService/EntityOwnershipServiceTestFixture.cpp
@@ -91,6 +92,8 @@ set(FILES
Prefab/SpawnableSortEntitiesTestFixture.cpp
Prefab/SpawnableSortEntitiesTestFixture.h
Prefab/SpawnableSortEntitiesTests.cpp
Prefab/PrefabScriptingTests.cpp
Prefab/ProceduralPrefabAssetTests.cpp
PropertyIntCtrlCommonTests.h
PropertyIntSliderCtrlTests.cpp
PropertyIntSpinCtrlTests.cpp
@@ -36,6 +36,7 @@
#include <AssetBuilderComponent.h>
#include <AssetBuilderInfo.h>
#include <AzCore/Interface/Interface.h>
#include <Entity/EntityUtilityComponent.h>
namespace AssetBuilder
{
@@ -80,6 +81,7 @@ AZ::ComponentTypeList AssetBuilderApplication::GetRequiredSystemComponents() con
azrtti_typeid<AzToolsFramework::Components::EditorEntityModelComponent>(),
azrtti_typeid<AzToolsFramework::EditorEntityContextComponent>(),
azrtti_typeid<AzToolsFramework::Prefab::PrefabSystemComponent>(),
azrtti_typeid<AzToolsFramework::EntityUtilityComponent>(),
});
return components;
@@ -62,7 +62,10 @@ namespace AssetProcessor
{
searchStr = searchStr.mid(0, subidPos);
}
AZ::Uuid filterAsUuid = AZ::Uuid::CreateStringPermissive(searchStr.toUtf8(), 0);
// Cap the string to some reasonable length, we don't want to try parsing an entire book
size_t cappedStringLength = searchStr.length() > 60 ? 60 : searchStr.length();
AZ::Uuid filterAsUuid = AZ::Uuid::CreateStringPermissive(searchStr.toUtf8(), cappedStringLength);
return DescendantMatchesFilter(*assetTreeItem, filter, filterAsUuid);
}
@@ -194,8 +194,8 @@ namespace AZ
if (baseClass)
{
m_behaviorClass = behaviorClass;
return true;
}
return true;
}
return false;
}
@@ -47,6 +47,16 @@ namespace AZ
return m_sourceGuid;
}
void Scene::SetWatchFolder(const AZStd::string& watchFolder)
{
m_watchFolder = watchFolder;
}
const AZStd::string& Scene::GetWatchFolder() const
{
return m_watchFolder;
}
void Scene::SetManifestFilename(const AZStd::string& name)
{
m_manifestFilename = name;
@@ -111,6 +121,7 @@ namespace AZ
->Property("sourceGuid", BehaviorValueGetter(&Scene::m_sourceGuid), nullptr)
->Property("graph", BehaviorValueGetter(&Scene::m_graph), nullptr)
->Property("manifest", BehaviorValueGetter(&Scene::m_manifest), nullptr)
->Property("watchFolder", BehaviorValueGetter(&Scene::m_watchFolder), nullptr)
->Constant("SceneOrientation_YUp", BehaviorConstant(SceneOrientation::YUp))
->Constant("SceneOrientation_ZUp", BehaviorConstant(SceneOrientation::ZUp))
->Constant("SceneOrientation_XUp", BehaviorConstant(SceneOrientation::XUp))
@@ -34,6 +34,9 @@ namespace AZ
const AZStd::string& GetSourceFilename() const;
const Uuid& GetSourceGuid() const;
void SetWatchFolder(const AZStd::string& watchFolder);
const AZStd::string& GetWatchFolder() const;
void SetManifestFilename(const AZStd::string& name);
void SetManifestFilename(AZStd::string&& name);
const AZStd::string& GetManifestFilename() const;
@@ -59,6 +62,7 @@ namespace AZ
AZStd::string m_name;
AZStd::string m_manifestFilename;
AZStd::string m_sourceFilename;
AZStd::string m_watchFolder;
Uuid m_sourceGuid;
SceneGraph m_graph;
SceneManifest m_manifest;
@@ -97,7 +97,7 @@ namespace AZ
GraphObjectProxy* proxy = aznew GraphObjectProxy(graphObject);
return proxy;
}
return nullptr;
return aznew GraphObjectProxy(nullptr);
});
}
}
@@ -36,8 +36,7 @@ namespace AZ
{
namespace Containers
{
//! Protects from allocating too much memory. The choice of a 5MB threshold is arbitrary.
const size_t MaxSceneManifestFileSizeInBytes = 5 * 1024 * 1024;
const char ErrorWindowName[] = "SceneManifest";
@@ -41,6 +41,8 @@ namespace AZ
AZ_RTTI(SceneManifest, "{9274AD17-3212-4651-9F3B-7DCCB080E467}");
static constexpr size_t MaxSceneManifestFileSizeInBytes = AZStd::numeric_limits<size_t>::max();
virtual ~SceneManifest();
static AZStd::shared_ptr<const DataTypes::IManifestObject> SceneManifestConstDataConverter(
@@ -6,6 +6,7 @@
*
*/
#include <API/EditorAssetSystemAPI.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/StringFunc/StringFunc.h>
@@ -73,6 +74,10 @@ namespace AZ
{
}
void AssetImportRequest::GetGeneratedManifestExtension(AZStd::string& /*result*/)
{
}
void AssetImportRequest::GetSupportedFileExtensions(AZStd::unordered_set<AZStd::string>& /*extensions*/)
{
}
@@ -103,14 +108,34 @@ namespace AZ
AZ_UNUSED(value);
}
void AssetImportRequest::GetManifestDependencyPaths(AZStd::vector<AZStd::string>&)
{
}
AZStd::shared_ptr<Containers::Scene> AssetImportRequest::LoadSceneFromVerifiedPath(const AZStd::string& assetFilePath, const Uuid& sourceGuid,
RequestingApplication requester, const Uuid& loadingComponentUuid)
RequestingApplication requester, const Uuid& loadingComponentUuid)
{
AZStd::string sceneName;
AzFramework::StringFunc::Path::GetFileName(assetFilePath.c_str(), sceneName);
AZStd::shared_ptr<Containers::Scene> scene = AZStd::make_shared<Containers::Scene>(AZStd::move(sceneName));
AZ_Assert(scene, "Unable to create new scene for asset importing.");
Data::AssetInfo assetInfo;
AZStd::string watchFolder;
bool result = false;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(result, &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourceUUID, sourceGuid, assetInfo, watchFolder);
if (result)
{
scene->SetWatchFolder(watchFolder);
}
else
{
AZ_Error(
"AssetImportRequest", false, "Failed to get watch folder for source %s",
sourceGuid.ToString<AZStd::string>().c_str());
}
// Unique pointer, will deactivate and clean up once going out of scope.
SceneCore::EntityConstructor::EntityPointer loaders =
SceneCore::EntityConstructor::BuildEntity("Scene Loading", loadingComponentUuid);
@@ -78,6 +78,8 @@ namespace AZ
virtual void GetSupportedFileExtensions(AZStd::unordered_set<AZStd::string>& extensions);
//! Gets the file extension for the manifest.
virtual void GetManifestExtension(AZStd::string& result);
//! Gets the file extension for the generated manifest.
virtual void GetGeneratedManifestExtension(AZStd::string& result);
//! Before asset loading starts this is called to allow for any required initialization.
virtual ProcessingResult PrepareForAssetLoading(Containers::Scene& scene, RequestingApplication requester);
@@ -97,6 +99,23 @@ namespace AZ
// Get scene processing project setting: UseCustomNormal
virtual void AreCustomNormalsUsed(bool & value);
/*!
Optional method for reporting source file dependencies that may exist in the scene manifest
Paths is a vector of JSON Path strings, relative to the IRule object
For example, the following path: /scriptFilename
Would match with this manifest:
{
"values": [
{
"$type": "Test",
"scriptFilename": "file.py"
}
]
}
*/
virtual void GetManifestDependencyPaths(AZStd::vector<AZStd::string>& paths);
//! Utility function to load an asset and manifest from file by using the EBus functions above.
//! @param assetFilePath The absolute path to the source file (not the manifest).
//! @param sourceGuid The guid assigned to the source file (not the manifest).
@@ -56,8 +56,14 @@ namespace AZ
result = s_extension;
}
void ManifestImportRequestHandler::GetGeneratedManifestExtension(AZStd::string& result)
{
result = s_extension;
result.append(s_generated);
}
Events::LoadingResult ManifestImportRequestHandler::LoadAsset(Containers::Scene& scene, const AZStd::string& path,
const Uuid& /*guid*/, RequestingApplication /*requester*/)
const Uuid& /*guid*/, RequestingApplication /*requester*/)
{
AZStd::string manifestPath = path + s_extension;
@@ -31,6 +31,7 @@ namespace AZ
static void Reflect(ReflectContext* context);
void GetManifestExtension(AZStd::string& result) override;
void GetGeneratedManifestExtension(AZStd::string& result) override;
Events::LoadingResult LoadAsset(Containers::Scene& scene, const AZStd::string& path, const Uuid& guid,
RequestingApplication requester) override;
@@ -19,6 +19,9 @@
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
#include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
#include <AzToolsFramework/Debug/TraceContext.h>
#include <Entity/EntityUtilityComponent.h>
#include <Prefab/PrefabSystemComponentInterface.h>
#include <Prefab/PrefabSystemScriptingBus.h>
#include <SceneAPI/SceneCore/Containers/Scene.h>
#include <SceneAPI/SceneCore/Containers/SceneGraph.h>
#include <SceneAPI/SceneCore/Containers/SceneManifest.h>
@@ -99,6 +102,7 @@ namespace AZ::SceneAPI::Behaviors
{
AZStd::string result;
CallResult(result, FN_OnUpdateManifest, scene);
ScriptBuildingNotificationBusHandler::BusDisconnect();
return result;
}
@@ -110,6 +114,7 @@ namespace AZ::SceneAPI::Behaviors
{
ExportProductList result;
CallResult(result, FN_OnPrepareForExport, scene, outputDirectory, platformIdentifier, productList);
ScriptBuildingNotificationBusHandler::BusDisconnect();
return result;
}
@@ -168,21 +173,9 @@ namespace AZ::SceneAPI::Behaviors
UnloadPython();
}
bool ScriptProcessorRuleBehavior::LoadPython(const AZ::SceneAPI::Containers::Scene& scene)
bool ScriptProcessorRuleBehavior::LoadPython(const AZ::SceneAPI::Containers::Scene& scene, AZStd::string& scriptPath)
{
if (m_editorPythonEventsInterface && !m_scriptFilename.empty())
{
return true;
}
// get project folder
auto settingsRegistry = AZ::SettingsRegistry::Get();
AZ::IO::FixedMaxPath projectPath;
if (!settingsRegistry->Get(projectPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath))
{
return false;
}
int scriptDiscoveryAttempts = 0;
const AZ::SceneAPI::Containers::SceneManifest& manifest = scene.GetManifest();
auto view = Containers::MakeDerivedFilterView<DataTypes::IScriptProcessorRule>(manifest.GetValueStorage());
for (const auto& scriptItem : view)
@@ -194,9 +187,21 @@ namespace AZ::SceneAPI::Behaviors
continue;
}
++scriptDiscoveryAttempts;
// check for file exist via absolute path
if (!IO::FileIOBase::GetInstance()->Exists(scriptFilename.c_str()))
{
// get project folder
auto settingsRegistry = AZ::SettingsRegistry::Get();
AZ::IO::FixedMaxPath projectPath;
if (!settingsRegistry->Get(projectPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath))
{
AZ_Error("scene", false, "With (%s) could not find Project Path during script discovery.",
scene.GetManifestFilename().c_str());
return false;
}
// check for script in the project folder
AZ::IO::FixedMaxPath projectScriptPath = projectPath / scriptFilename;
if (!IO::FileIOBase::GetInstance()->Exists(projectScriptPath.c_str()))
@@ -209,32 +214,47 @@ namespace AZ::SceneAPI::Behaviors
scriptFilename = AZStd::move(projectScriptPath);
}
// lazy load the Python interface
auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
if (editorPythonEventsInterface->IsPythonActive() == false)
{
const bool silenceWarnings = false;
if (editorPythonEventsInterface->StartPython(silenceWarnings) == false)
{
editorPythonEventsInterface = nullptr;
}
}
scriptPath = scriptFilename.c_str();
break;
}
// both Python and the script need to be ready
if (editorPythonEventsInterface == nullptr || scriptFilename.empty())
{
AZ_Warning("scene", false,"The scene manifest (%s) attempted to use script(%s) but Python is not enabled;"
"please add the EditorPythonBinding gem & PythonAssetBuilder gem to your project.",
scene.GetManifestFilename().c_str(), scriptFilename.c_str());
if (scriptPath.empty())
{
AZ_Warning("scene", scriptDiscoveryAttempts == 0,
"The scene manifest (%s) attempted to use script rule, but no script file path could be found.",
scene.GetManifestFilename().c_str());
return false;
}
return false;
}
m_editorPythonEventsInterface = editorPythonEventsInterface;
m_scriptFilename = scriptFilename.c_str();
// already prepared the Python VM?
if (m_editorPythonEventsInterface)
{
return true;
}
return false;
// lazy load the Python interface
auto editorPythonEventsInterface = AZ::Interface<AzToolsFramework::EditorPythonEventsInterface>::Get();
if (editorPythonEventsInterface->IsPythonActive() == false)
{
const bool silenceWarnings = false;
if (editorPythonEventsInterface->StartPython(silenceWarnings) == false)
{
editorPythonEventsInterface = nullptr;
}
}
// both Python and the script need to be ready
if (editorPythonEventsInterface == nullptr)
{
AZ_Warning("scene", false,
"The scene manifest (%s) attempted to prepare Python but Python can not start",
scene.GetManifestFilename().c_str());
return false;
}
m_editorPythonEventsInterface = editorPythonEventsInterface;
return true;
}
void ScriptProcessorRuleBehavior::UnloadPython()
@@ -251,11 +271,13 @@ namespace AZ::SceneAPI::Behaviors
{
using namespace AzToolsFramework;
auto executeCallback = [this, &context]()
AZStd::string scriptPath;
auto executeCallback = [&context, &scriptPath]()
{
// set up script's hook callback
EditorPythonRunnerRequestBus::Broadcast(&EditorPythonRunnerRequestBus::Events::ExecuteByFilename,
m_scriptFilename.c_str());
scriptPath.c_str());
// call script's callback to allow extra products
ExportProductList extraProducts;
@@ -279,7 +301,7 @@ namespace AZ::SceneAPI::Behaviors
}
};
if (LoadPython(context.GetScene()))
if (LoadPython(context.GetScene(), scriptPath))
{
EditorPythonConsoleNotificationHandler logger;
m_editorPythonEventsInterface->ExecuteWithLock(executeCallback);
@@ -306,23 +328,19 @@ namespace AZ::SceneAPI::Behaviors
{
using namespace AzToolsFramework;
// This behavior persists on the same AssetBuilder. Clear the script file name so that if
// this builder processes a scene file with a script file name, and then later processes
// a scene without a script file name, it won't run the old script on the new scene.
m_scriptFilename.clear();
if (action != ManifestAction::Update)
{
return Events::ProcessingResult::Ignored;
}
if (LoadPython(scene))
AZStd::string scriptPath;
if (LoadPython(scene, scriptPath))
{
AZStd::string manifestUpdate;
auto executeCallback = [this, &scene, &manifestUpdate]()
auto executeCallback = [&scene, &manifestUpdate, &scriptPath]()
{
EditorPythonRunnerRequestBus::Broadcast(&EditorPythonRunnerRequestBus::Events::ExecuteByFilename,
m_scriptFilename.c_str());
scriptPath.c_str());
ScriptBuildingNotificationBus::BroadcastResult(manifestUpdate, &ScriptBuildingNotificationBus::Events::OnUpdateManifest,
scene);
@@ -331,6 +349,9 @@ namespace AZ::SceneAPI::Behaviors
EditorPythonConsoleNotificationHandler logger;
m_editorPythonEventsInterface->ExecuteWithLock(executeCallback);
EntityUtilityBus::Broadcast(&EntityUtilityBus::Events::ResetEntityContext);
AZ::Interface<Prefab::PrefabSystemComponentInterface>::Get()->RemoveAllTemplates();
// attempt to load the manifest string back to a JSON-scene-manifest
auto sceneManifestLoader = AZStd::make_unique<AZ::SceneAPI::Containers::SceneManifest>();
auto loadOutcome = sceneManifestLoader->LoadFromString(manifestUpdate);
@@ -347,4 +368,8 @@ namespace AZ::SceneAPI::Behaviors
return Events::ProcessingResult::Ignored;
}
void ScriptProcessorRuleBehavior::GetManifestDependencyPaths(AZStd::vector<AZStd::string>& paths)
{
paths.emplace_back("/scriptFilename");
}
} // namespace AZ
@@ -37,6 +37,7 @@ namespace AZ::SceneAPI::Behaviors
, public Events::AssetImportRequestBus::Handler
{
public:
AZ_COMPONENT(ScriptProcessorRuleBehavior, "{24054E73-1B92-43B0-AC13-174B2F0E3F66}", SceneCore::BehaviorComponent);
~ScriptProcessorRuleBehavior() override = default;
@@ -44,22 +45,21 @@ namespace AZ::SceneAPI::Behaviors
SCENE_DATA_API void Activate() override;
SCENE_DATA_API void Deactivate() override;
static void Reflect(ReflectContext* context);
// AssetImportRequestBus::Handler
SCENE_DATA_API Events::ProcessingResult UpdateManifest(
Containers::Scene& scene,
ManifestAction action,
RequestingApplication requester) override;
SCENE_DATA_API void GetManifestDependencyPaths(AZStd::vector<AZStd::string>& paths) override;
protected:
bool LoadPython(const AZ::SceneAPI::Containers::Scene& scene);
bool LoadPython(const AZ::SceneAPI::Containers::Scene& scene, AZStd::string& scriptPath);
void UnloadPython();
bool DoPrepareForExport(Events::PreExportEventContext& context);
private:
AzToolsFramework::EditorPythonEventsInterface* m_editorPythonEventsInterface = nullptr;
AZStd::string m_scriptFilename;
struct ExportEventHandler;
AZStd::shared_ptr<ExportEventHandler> m_exportEventHandler;
@@ -147,22 +147,6 @@
},
"LoadAction": "Clear"
}
},
{
"Name": "ScatterDistanceOutput",
"SlotType": "Output",
"ScopeAttachmentUsage": "RenderTarget",
"LoadStoreAction": {
"ClearValue": {
"Value": [
0.0,
0.0,
0.0,
0.0
]
},
"LoadAction": "Clear"
}
}
],
"ImageAttachments": [
@@ -257,23 +241,6 @@
"AssetRef": {
"FilePath": "Textures/BRDFTexture.attimage"
}
},
{
"Name": "ScatterDistanceImage",
"SizeSource": {
"Source": {
"Pass": "Parent",
"Attachment": "Output"
}
},
"MultisampleSource": {
"Pass": "This",
"Attachment": "DepthStencilInputOutput"
},
"ImageDescriptor": {
"Format": "R11G11B10_FLOAT",
"SharedQueueMask": "Graphics"
}
}
],
"Connections": [
@@ -318,13 +285,6 @@
"Pass": "This",
"Attachment": "BRDFTexture"
}
},
{
"LocalSlot": "ScatterDistanceOutput",
"AttachmentRef": {
"Pass": "This",
"Attachment": "ScatterDistanceImage"
}
}
]
}
@@ -15,7 +15,6 @@
// box-filtered from the MSAA sub-pixels of the reflection texture.
#include <scenesrg.srgi>
#include <viewsrg.srgi>
#include <Atom/Features/PostProcessing/FullscreenVertexUtil.azsli>
#include <Atom/Features/PostProcessing/FullscreenVertexInfo.azsli>
@@ -26,8 +25,6 @@ ShaderResourceGroup PassSrg : SRG_PerPass
Texture2DMS<float4> m_reflection;
}
#include <Atom/RPI/ShaderResourceGroups/DefaultDrawSrg.azsli>
// Vertex Shader
VSOutput MainVS(VSInput input)
{
@@ -286,8 +286,11 @@ namespace Blast
DefaultConfigurationPath, globalConfiguration);
AZ_Warning("Blast", loaded, "Failed to load Blast configuration, initializing with default configs.");
SetGlobalConfiguration(globalConfiguration);
SaveConfiguration();
ApplyGlobalConfiguration(globalConfiguration);
if (!loaded)
{
SaveConfiguration();
}
}
void BlastSystemComponent::SaveConfiguration()
@@ -394,8 +397,13 @@ namespace Blast
void BlastSystemComponent::SetGlobalConfiguration(const BlastGlobalConfiguration& globalConfiguration)
{
m_configuration = globalConfiguration;
ApplyGlobalConfiguration(globalConfiguration);
SaveConfiguration();
}
void BlastSystemComponent::ApplyGlobalConfiguration(const BlastGlobalConfiguration& globalConfiguration)
{
m_configuration = globalConfiguration;
{
AZ::Data::Asset<Blast::BlastMaterialLibraryAsset>& materialLibrary = m_configuration.m_materialLibrary;
@@ -93,6 +93,7 @@ namespace Blast
void InitPhysics();
void DeactivatePhysics();
void ApplyGlobalConfiguration(const BlastGlobalConfiguration& materialLibrary);
void RegisterCommands();
// Internal helper functions & classes
+1
View File
@@ -36,6 +36,7 @@ ly_add_target(
PUBLIC
AZ::AtomCore
Gem::Atom_RPI.Public
Gem::AtomLyIntegration_CommonFeatures.Static
Gem::LmbrCentral
COMPILE_DEFINITIONS
PUBLIC
@@ -142,12 +142,31 @@ namespace EMotionFX
{
ActorComponentNotificationBus::Handler::BusConnect(GetEntityId());
AZ::TickBus::Handler::BusConnect();
// Remember the lod type and level so that we can set it back to the previous one on deactivation of the component.
AZ::Render::MeshComponentRequestBus::EventResult(m_previousLodType,
GetEntityId(),
&AZ::Render::MeshComponentRequestBus::Events::GetLodType);
if (m_actorInstance)
{
m_previousLodLevel = m_actorInstance->GetLODLevel();
}
}
void SimpleLODComponent::Deactivate()
{
AZ::TickBus::Handler::BusDisconnect();
ActorComponentNotificationBus::Handler::BusDisconnect();
AZ::Render::MeshComponentRequestBus::Event(GetEntityId(),
&AZ::Render::MeshComponentRequestBus::Events::SetLodType,
m_previousLodType);
if (m_actorInstance)
{
m_actorInstance->SetLODLevel(m_previousLodLevel);
}
}
void SimpleLODComponent::OnActorInstanceCreated(EMotionFX::ActorInstance* actorInstance)
@@ -183,7 +202,7 @@ namespace EMotionFX
return max - 1;
}
void SimpleLODComponent::UpdateLodLevelByDistance(EMotionFX::ActorInstance * actorInstance, const Configuration& configuration, AZ::EntityId entityId)
void SimpleLODComponent::UpdateLodLevelByDistance(EMotionFX::ActorInstance* actorInstance, const Configuration& configuration, AZ::EntityId entityId)
{
if (actorInstance)
{
@@ -201,15 +220,30 @@ namespace EMotionFX
AZ::RPI::ViewportContextPtr defaultViewportContext =
viewportContextManager->GetViewportContextByName(viewportContextManager->GetDefaultViewportContextName());
const float distance = worldPos.GetDistance(defaultViewportContext->GetCameraTransform().GetTranslation());
const size_t lodByDistance = GetLodByDistance(configuration.m_lodDistances, distance);
actorInstance->SetLODLevel(lodByDistance);
const size_t requestedLod = GetLodByDistance(configuration.m_lodDistances, distance);
actorInstance->SetLODLevel(requestedLod);
if (configuration.m_enableLodSampling)
{
const float animGraphSampleRate = configuration.m_lodSampleRates[lodByDistance];
const float animGraphSampleRate = configuration.m_lodSampleRates[requestedLod];
const float updateRateInSeconds = animGraphSampleRate > 0.0f ? 1.0f / animGraphSampleRate : 0.0f;
actorInstance->SetMotionSamplingRate(updateRateInSeconds);
}
// Disable the automatic mesh LOD level adjustment based on screen space in case a simple LOD component is present.
// The simple LOD component overrides the mesh LOD level and syncs the skeleton with the mesh LOD level.
AZ::Render::MeshComponentRequestBus::Event(entityId,
&AZ::Render::MeshComponentRequestBus::Events::SetLodType,
AZ::RPI::Cullable::LodType::SpecificLod);
// When setting the actor instance LOD level, a change is just requested and with the next update it will get applied.
// This means that the current LOD level might differ from the requested one. We need to sync the Atom LOD level with the
// current LOD level of the actor instance to avoid skinning artifacts. The requested LOD level will be present and applied
// the following frame.
const size_t currentLod = actorInstance->GetLODLevel();
AZ::Render::MeshComponentRequestBus::Event(entityId,
&AZ::Render::MeshComponentRequestBus::Events::SetLodOverride,
static_cast<AZ::RPI::Cullable::LodOverride>(currentLod));
}
}
} // namespace integration
@@ -17,7 +17,7 @@
#include <Integration/Assets/MotionAsset.h>
#include <Integration/ActorComponentBus.h>
#include <AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h>
namespace EMotionFX
{
@@ -93,6 +93,9 @@ namespace EMotionFX
Configuration m_configuration; // Component configuration.
EMotionFX::ActorInstance* m_actorInstance; // Associated actor instance (retrieved from Actor Component).
AZ::RPI::Cullable::LodType m_previousLodType = AZ::RPI::Cullable::LodType::Default;
size_t m_previousLodLevel = 0;
};
} // namespace Integration
@@ -8,7 +8,9 @@
#include "EditorSystemComponent.h"
#include <AzCore/Interface/Interface.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzFramework/Physics/SystemBus.h>
#include <AzFramework/Physics/Collision/CollisionEvents.h>
#include <AzFramework/Physics/Common/PhysicsSimulatedBody.h>
@@ -23,7 +25,7 @@
namespace PhysX
{
constexpr const char* DefaultAssetFilePath = "Physics/SurfaceTypeMaterialLibrary";
constexpr const char* DefaultAssetFilePath = "Assets/Physics/SurfaceTypeMaterialLibrary";
constexpr const char* TemplateAssetFilename = "PhysX/TemplateMaterialLibrary";
static AZStd::optional<AZ::Data::Asset<AZ::Data::AssetData>> GetMaterialLibraryTemplate()
@@ -67,7 +69,7 @@ namespace PhysX
assetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, relativePath.c_str(), assetType, true /*autoRegisterIfNotFound*/);
AZ::Data::Asset<AZ::Data::AssetData> newAsset =
AZ::Data::AssetManager::Instance().GetAsset(assetId, assetType, AZ::Data::AssetLoadBehavior::Default);
AZ::Data::AssetManager::Instance().FindOrCreateAsset(assetId, assetType, AZ::Data::AssetLoadBehavior::Default);
if (auto* newMaterialLibraryData = azrtti_cast<Physics::MaterialLibraryAsset*>(newAsset.GetData()))
{
@@ -138,6 +140,14 @@ namespace PhysX
if (auto retrievedMaterialLibrary = RetrieveDefaultMaterialLibrary())
{
physxSystem->UpdateMaterialLibrary(retrievedMaterialLibrary.value());
// After setting the default material library, save the physx configuration.
auto saveCallback = []([[maybe_unused]] const PhysXSystemConfiguration& config, [[maybe_unused]] PhysXSettingsRegistryManager::Result result)
{
AZ_Warning("PhysX", result == PhysXSettingsRegistryManager::Result::Success,
"Unable to save the PhysX configuration after setting default material library.");
};
physxSystem->GetSettingsRegistryManager().SaveSystemConfiguration(physxSystem->GetPhysXConfiguration(), saveCallback);
}
}
}
@@ -236,11 +246,15 @@ namespace PhysX
if (!resultAssetId.IsValid())
{
// No file for the default material library, create it
const char* assetRoot = AZ::IO::FileIOBase::GetInstance()->GetAlias("@projectsourceassets@");
AZStd::string fullPath;
AzFramework::StringFunc::Path::ConstructFull(assetRoot, DefaultAssetFilePath, assetExtension.c_str(), fullPath);
AZ::IO::Path fullPath;
if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
{
settingsRegistry->Get(fullPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath);
}
fullPath /= DefaultAssetFilePath;
fullPath.ReplaceExtension(AZ::IO::PathView(assetExtension));
if (auto materialLibraryOpt = CreateMaterialLibrary(fullPath, relativePath))
if (auto materialLibraryOpt = CreateMaterialLibrary(fullPath.Native(), relativePath))
{
return materialLibraryOpt;
}
@@ -511,10 +511,12 @@ namespace PhysX
materialLibrary.BlockUntilLoadComplete();
AZ_Warning("PhysX", (materialLibrary.GetData() != nullptr),
const bool loadedSuccessfully = materialLibrary.GetData() != nullptr && !materialLibrary.IsError();
AZ_Warning("PhysX", loadedSuccessfully,
"LoadDefaultMaterialLibrary: Default Material Library asset data is invalid.");
return materialLibrary.GetData() != nullptr && !materialLibrary.IsError();
return loadedSuccessfully;
}
//TEMP -- until these are fully moved over here
+21 -3
View File
@@ -458,7 +458,13 @@ namespace PhysX
{
const PhysXSystemConfiguration defaultConfig = PhysXSystemConfiguration::CreateDefault();
m_physXSystem->Initialize(&defaultConfig);
registryManager.SaveSystemConfiguration(defaultConfig, {});
auto saveCallback = []([[maybe_unused]] const PhysXSystemConfiguration& config, [[maybe_unused]] PhysXSettingsRegistryManager::Result result)
{
AZ_Warning("PhysX", result == PhysXSettingsRegistryManager::Result::Success,
"Unable to save the default PhysX configuration.");
};
registryManager.SaveSystemConfiguration(defaultConfig, saveCallback);
}
//Load the DefaultSceneConfig
@@ -471,7 +477,13 @@ namespace PhysX
{
const AzPhysics::SceneConfiguration defaultConfig = AzPhysics::SceneConfiguration::CreateDefault();
m_physXSystem->UpdateDefaultSceneConfiguration(defaultConfig);
registryManager.SaveDefaultSceneConfiguration(defaultConfig, {});
auto saveCallback = []([[maybe_unused]] const AzPhysics::SceneConfiguration& config, [[maybe_unused]] PhysXSettingsRegistryManager::Result result)
{
AZ_Warning("PhysX", result == PhysXSettingsRegistryManager::Result::Success,
"Unable to save the default Scene configuration.");
};
registryManager.SaveDefaultSceneConfiguration(defaultConfig, saveCallback);
}
//load the debug configuration and initialize the PhysX debug interface
@@ -486,7 +498,13 @@ namespace PhysX
{
const Debug::DebugConfiguration defaultConfig = Debug::DebugConfiguration::CreateDefault();
debug->Initialize(defaultConfig);
registryManager.SaveDebugConfiguration(defaultConfig, {});
auto saveCallback = []([[maybe_unused]] const Debug::DebugConfiguration& config, [[maybe_unused]] PhysXSettingsRegistryManager::Result result)
{
AZ_Warning("PhysX", result == PhysXSettingsRegistryManager::Result::Success,
"Unable to save the default PhysX Debug configuration.");
};
registryManager.SaveDebugConfiguration(defaultConfig, saveCallback);
}
}
}
+8 -1
View File
@@ -13,6 +13,9 @@ endif()
ly_add_target(
NAME PrefabBuilder.Static STATIC
NAMESPACE Gem
INCLUDE_DIRECTORIES
PRIVATE
.
FILES_CMAKE
prefabbuilder_files.cmake
BUILD_DEPENDENCIES
@@ -20,6 +23,9 @@ ly_add_target(
AZ::AzCore
AZ::AzToolsFramework
AZ::AssetBuilderSDK
AZ::SceneCore
AZ::SceneData
3rdParty::RapidJSON
)
ly_add_target(
@@ -35,7 +41,8 @@ ly_add_target(
Gem::PrefabBuilder.Static
)
# the prefab builder only needs to be active in builders
# create an alias for the tool version
ly_create_alias(NAME PrefabBuilder.Tools NAMESPACE Gem TARGETS Gem::PrefabBuilder.Builders)
# we automatically add this gem, if it is present, to all our known set of builder applications:
ly_enable_gems(GEMS PrefabBuilder)
@@ -8,6 +8,7 @@
#include <AzCore/Module/Module.h>
#include <PrefabBuilderComponent.h>
#include <PrefabGroup/PrefabGroupBehavior.h>
namespace AZ::Prefab
{
@@ -22,7 +23,8 @@ namespace AZ::Prefab
: Module()
{
m_descriptors.insert(m_descriptors.end(), {
PrefabBuilderComponent::CreateDescriptor()
PrefabBuilderComponent::CreateDescriptor(),
AZ::SceneAPI::Behaviors::PrefabGroupBehavior::CreateDescriptor()
});
}
};
@@ -13,6 +13,7 @@
#include <Application/ToolsApplication.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/Asset/AssetSerializer.h>
namespace UnitTest
{
@@ -0,0 +1,25 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/RTTI/RTTI.h>
#include <SceneAPI/SceneCore/DataTypes/Groups/ISceneNodeGroup.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
namespace AZ::SceneAPI::DataTypes
{
class IPrefabGroup
: public ISceneNodeGroup
{
public:
AZ_RTTI(IPrefabGroup, "{7E50FAEF-3379-4521-99C5-B428FDEE3B7B}", ISceneNodeGroup);
~IPrefabGroup() override = default;
virtual AzToolsFramework::Prefab::PrefabDomConstReference GetPrefabDomRef() const = 0;
};
}
@@ -0,0 +1,161 @@
/*
* 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 <PrefabBuilderTests.h>
#include <PrefabGroup/IPrefabGroup.h>
#include <PrefabGroup/PrefabGroup.h>
#include <PrefabGroup/PrefabGroupBehavior.h>
#include <AzTest/Utils.h>
#include <Tests/AssetSystemMocks.h>
#include <AzCore/Serialization/Json/JsonSystemComponent.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzToolsFramework/Asset/AssetSystemComponent.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
#include <SceneAPI/SceneCore/Containers/Scene.h>
#include <SceneAPI/SceneCore/Events/CallProcessorBus.h>
#include <SceneAPI/SceneCore/Events/ExportEventContext.h>
#include <SceneAPI/SceneCore/Events/ExportProductList.h>
#include <SceneAPI/SceneCore/SceneCoreStandaloneAllocator.h>
#include <PrefabGroup/PrefabBehaviorTests.inl>
namespace UnitTest
{
class PrefabBehaviorTests
: public PrefabBuilderTests
{
public:
static void SetUpTestCase()
{
// Allocator needed by SceneCore
if (!AZ::AllocatorInstance<AZ::SystemAllocator>().IsReady())
{
AZ::AllocatorInstance<AZ::SystemAllocator>().Create();
}
AZ::SceneAPI::SceneCoreStandaloneAllocator::Initialize(AZ::Environment::GetInstance());
}
static void TearDownTestCase()
{
AZ::SceneAPI::SceneCoreStandaloneAllocator::TearDown();
AZ::AllocatorInstance<AZ::SystemAllocator>().Destroy();
}
void SetUp() override
{
PrefabBuilderTests::SetUp();
m_prefabGroupBehavior = AZStd::make_unique<AZ::SceneAPI::Behaviors::PrefabGroupBehavior>();
m_prefabGroupBehavior->Activate();
// Mocking the asset system replacing the AssetSystem::AssetSystemComponent
AZ::Entity* systemEntity = m_app.FindEntity(AZ::SystemEntityId);
systemEntity->FindComponent<AzToolsFramework::AssetSystem::AssetSystemComponent>()->Deactivate();
using namespace testing;
ON_CALL(m_assetSystemRequestMock, GetSourceInfoBySourcePath(_, _, _)).WillByDefault([](auto* path, auto& info, auto&)
{
return PrefabBehaviorTests::OnGetSourceInfoBySourcePath(path, info);
});
m_assetSystemRequestMock.BusConnect();
}
void TearDown() override
{
m_assetSystemRequestMock.BusDisconnect();
m_prefabGroupBehavior->Deactivate();
m_prefabGroupBehavior.reset();
PrefabBuilderTests::TearDown();
}
static bool OnGetSourceInfoBySourcePath(AZStd::string_view sourcePath, AZ::Data::AssetInfo& assetInfo)
{
if (sourcePath == AZStd::string_view("mock"))
{
assetInfo.m_assetId = AZ::Uuid::CreateRandom();
assetInfo.m_assetType = azrtti_typeid<AZ::Prefab::ProceduralPrefabAsset>();
assetInfo.m_relativePath = "mock/path";
assetInfo.m_sizeBytes = 0;
}
return true;
}
struct TestPreExportEventContext
{
TestPreExportEventContext()
: m_scene("test_context")
{
using namespace AZ::SceneAPI::Events;
m_preExportEventContext = AZStd::make_unique<PreExportEventContext>(m_productList, m_outputDirectory, m_scene, "mock");
}
void SetOutputDirectory(AZStd::string outputDirectory)
{
using namespace AZ::SceneAPI::Events;
m_outputDirectory = AZStd::move(outputDirectory);
m_preExportEventContext = AZStd::make_unique<PreExportEventContext>(m_productList, m_outputDirectory, m_scene, "mock");
}
AZStd::unique_ptr<AZ::SceneAPI::Events::PreExportEventContext> m_preExportEventContext;
AZ::SceneAPI::Events::ExportProductList m_productList;
AZStd::string m_outputDirectory;
AZ::SceneAPI::Containers::Scene m_scene;
};
AZStd::unique_ptr<AZ::SceneAPI::Behaviors::PrefabGroupBehavior> m_prefabGroupBehavior;
testing::NiceMock<UnitTests::MockAssetSystemRequest> m_assetSystemRequestMock;
};
TEST_F(PrefabBehaviorTests, PrefabBehavior_EmptyContextIgnored_Works)
{
auto context = TestPreExportEventContext{};
auto result = AZ::SceneAPI::Events::ProcessingResult::Failure;
AZ::SceneAPI::Events::CallProcessorBus::BroadcastResult(
result,
&AZ::SceneAPI::Events::CallProcessorBus::Events::Process,
context.m_preExportEventContext.get());
EXPECT_EQ(result, AZ::SceneAPI::Events::ProcessingResult::Ignored);
}
TEST_F(PrefabBehaviorTests, PrefabBehavior_SimplePrefab_Works)
{
auto context = TestPreExportEventContext{};
// check for the file at <temp_directory>/mock/fake_prefab.procprefab
AZ::Test::ScopedAutoTempDirectory tempDir;
context.SetOutputDirectory(tempDir.GetDirectory());
auto jsonOutcome = AZ::JsonSerializationUtils::ReadJsonString(Data::jsonPrefab);
ASSERT_TRUE(jsonOutcome);
auto prefabGroup = AZStd::make_shared<AZ::SceneAPI::SceneData::PrefabGroup>();
prefabGroup.get()->SetId(AZ::Uuid::CreateRandom());
prefabGroup.get()->SetName("fake_prefab");
prefabGroup.get()->SetPrefabDom(AZStd::move(jsonOutcome.GetValue()));
context.m_scene.GetManifest().AddEntry(prefabGroup);
context.m_scene.SetSource("mock", AZ::Uuid::CreateRandom());
auto result = AZ::SceneAPI::Events::ProcessingResult::Failure;
AZ::SceneAPI::Events::CallProcessorBus::BroadcastResult(
result,
&AZ::SceneAPI::Events::CallProcessorBus::Events::Process,
context.m_preExportEventContext.get());
EXPECT_EQ(result, AZ::SceneAPI::Events::ProcessingResult::Success);
AZStd::string pathStr;
AzFramework::StringFunc::Path::ConstructFull(tempDir.GetDirectory(), "mock/fake_prefab.procprefab", pathStr, true);
if (!AZ::IO::SystemFile::Exists(pathStr.c_str()))
{
AZ_Warning("testing", false, "The product asset (%s) is missing", pathStr.c_str());
}
}
}
@@ -0,0 +1,104 @@
/*
* 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
*
*/
namespace UnitTest
{
namespace Data
{
const char* jsonPrefab = R"JSON(
{
"ContainerEntity": {
"Id": "ContainerEntity",
"Name": "test_template_1",
"Components": {
"Component_[12122553907433030840]": {
"$type": "EditorVisibilityComponent",
"Id": 12122553907433030840
},
"Component_[5666150279650800686]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 5666150279650800686,
"Parent Entity": ""
},
"Component_[8790726658974076423]": {
"$type": "EditorOnlyEntityComponent",
"Id": 8790726658974076423
}
}
},
"Entities": {
"Entity_[1588652751483]": {
"Id": "Entity_[1588652751483]",
"Name": "root",
"Components": {
"Component_[11872748096995986607]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 11872748096995986607,
"Parent Entity": "ContainerEntity",
"Transform Data": {
"Rotate": [
0.0,
0.10000000149011612,
180.0
]
}
},
"Component_[12138841758570858610]": {
"$type": "EditorVisibilityComponent",
"Id": 12138841758570858610
},
"Component_[15735658354806796004]": {
"$type": "EditorOnlyEntityComponent",
"Id": 15735658354806796004
}
}
},
"Entity_[1592947718779]": {
"Id": "Entity_[1592947718779]",
"Name": "cube",
"Components": {
"Component_[2505301170249328189]": {
"$type": "EditorOnlyEntityComponent",
"Id": 2505301170249328189
},
"Component_[3716170894544198343]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 3716170894544198343,
"Parent Entity": "Entity_[1588652751483]"
},
"Component_[5862175558847453681]": {
"$type": "EditorVisibilityComponent",
"Id": 5862175558847453681
}
}
},
"Entity_[1597242686075]": {
"Id": "Entity_[1597242686075]",
"Name": "cubeKid",
"Components": {
"Component_[10128771992421174485]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 10128771992421174485,
"Parent Entity": "Entity_[1592947718779]"
},
"Component_[14936165953779771344]": {
"$type": "EditorVisibilityComponent",
"Id": 14936165953779771344
},
"Component_[403416213715997356]": {
"$type": "EditorOnlyEntityComponent",
"Id": 403416213715997356
}
}
}
}
}
)JSON";
}
}
@@ -0,0 +1,136 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <PrefabGroup/PrefabGroup.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/JSON/error/error.h>
#include <AzCore/JSON/error/en.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/std/optional.h>
#include <AzFramework/FileFunc/FileFunc.h>
namespace AZ::SceneAPI::SceneData
{
// PrefabGroup
PrefabGroup::PrefabGroup()
: m_id(Uuid::CreateNull())
, m_name()
{
}
const AZStd::string& PrefabGroup::GetName() const
{
return m_name;
}
void PrefabGroup::SetName(AZStd::string name)
{
m_name = AZStd::move(name);
}
const Uuid& PrefabGroup::GetId() const
{
return m_id;
}
void PrefabGroup::SetId(Uuid id)
{
m_id = AZStd::move(id);
}
Containers::RuleContainer& PrefabGroup::GetRuleContainer()
{
return m_rules;
}
const Containers::RuleContainer& PrefabGroup::GetRuleContainerConst() const
{
return m_rules;
}
DataTypes::ISceneNodeSelectionList& PrefabGroup::GetSceneNodeSelectionList()
{
return m_nodeSelectionList;
}
const DataTypes::ISceneNodeSelectionList& PrefabGroup::GetSceneNodeSelectionList() const
{
return m_nodeSelectionList;
}
void PrefabGroup::SetPrefabDom(AzToolsFramework::Prefab::PrefabDom prefabDom)
{
m_prefabDomData = AZStd::make_shared<Prefab::PrefabDomData>();
m_prefabDomData->CopyValue(prefabDom);
}
AzToolsFramework::Prefab::PrefabDomConstReference PrefabGroup::GetPrefabDomRef() const
{
if (m_prefabDomData)
{
return m_prefabDomData->GetValue();
}
return {};
}
void PrefabGroup::Reflect(ReflectContext* context)
{
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<DataTypes::IPrefabGroup, DataTypes::ISceneNodeGroup>()
->Version(1);
serializeContext->Class<PrefabGroup, DataTypes::IPrefabGroup>()
->Version(1)
->Field("name", &PrefabGroup::m_name)
->Field("nodeSelectionList", &PrefabGroup::m_nodeSelectionList)
->Field("rules", &PrefabGroup::m_rules)
->Field("id", &PrefabGroup::m_id)
->Field("prefabDomData", &PrefabGroup::m_prefabDomData);
}
BehaviorContext* behaviorContext = azrtti_cast<BehaviorContext*>(context);
if (behaviorContext)
{
auto setPrefabDomData = [](PrefabGroup& self, const AZStd::string& json)
{
auto jsonOutcome = JsonSerializationUtils::ReadJsonString(json);
if (jsonOutcome.IsSuccess())
{
self.SetPrefabDom(AZStd::move(jsonOutcome.GetValue()));
return true;
}
AZ_Error("prefab", false, "Set PrefabDom failed (%s)", jsonOutcome.GetError().c_str());
return false;
};
auto getPrefabDomData = [](const PrefabGroup& self) -> AZStd::string
{
if (self.GetPrefabDomRef().has_value() == false)
{
return {};
}
AZStd::string buffer;
JsonSerializationUtils::WriteJsonString(self.GetPrefabDomRef().value(), buffer);
return buffer;
};
behaviorContext->Class<PrefabGroup>()
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All)
->Attribute(Script::Attributes::Scope, Script::Attributes::ScopeFlags::Common)
->Attribute(Script::Attributes::Module, "prefab")
->Property("name", BehaviorValueProperty(&PrefabGroup::m_name))
->Property("id", BehaviorValueProperty(&PrefabGroup::m_id))
->Property("prefabDomData", getPrefabDomData, setPrefabDomData);
}
}
}
@@ -0,0 +1,64 @@
/*
* 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 <PrefabGroup/IPrefabGroup.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <SceneAPI/SceneCore/Containers/RuleContainer.h>
#include <SceneAPI/SceneData/ManifestBase/SceneNodeSelectionList.h>
namespace AZ
{
class ReflectContext;
}
namespace AZ::SceneAPI::Containers
{
class Scene;
}
namespace AZ::SceneAPI::SceneData
{
class PrefabGroup final
: public DataTypes::IPrefabGroup
{
public:
AZ_RTTI(PrefabGroup, "{99FE3C6F-5B55-4D8B-8013-2708010EC715}", DataTypes::IPrefabGroup);
AZ_CLASS_ALLOCATOR(PrefabGroup, SystemAllocator, 0);
static void Reflect(AZ::ReflectContext* context);
PrefabGroup();
~PrefabGroup() override = default;
// DataTypes::IPrefabGroup
AzToolsFramework::Prefab::PrefabDomConstReference GetPrefabDomRef() const override;
const AZStd::string& GetName() const override;
const Uuid& GetId() const override;
Containers::RuleContainer& GetRuleContainer() override;
const Containers::RuleContainer& GetRuleContainerConst() const override;
DataTypes::ISceneNodeSelectionList& GetSceneNodeSelectionList() override;
const DataTypes::ISceneNodeSelectionList& GetSceneNodeSelectionList() const override;
// Concrete API
void SetId(Uuid id);
void SetName(AZStd::string name);
void SetPrefabDom(AzToolsFramework::Prefab::PrefabDom prefabDom);
private:
SceneNodeSelectionList m_nodeSelectionList;
Containers::RuleContainer m_rules;
AZStd::string m_name;
Uuid m_id;
AZStd::shared_ptr<Prefab::PrefabDomData> m_prefabDomData;
};
}
@@ -0,0 +1,251 @@
/*
* 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 <PrefabGroup/PrefabGroupBehavior.h>
#include <PrefabGroup/PrefabGroup.h>
#include <PrefabGroup/ProceduralAssetHandler.h>
#include <AzCore/Asset/AssetManagerBus.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/JSON/document.h>
#include <AzCore/JSON/error/en.h>
#include <AzCore/JSON/error/error.h>
#include <AzCore/JSON/prettywriter.h>
#include <AzCore/JSON/stringbuffer.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/Prefab/PrefabLoaderInterface.h>
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
#include <AzToolsFramework/Prefab/Instance/InstanceToTemplateInterface.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
#include <SceneAPI/SceneCore/Components/ExportingComponent.h>
#include <SceneAPI/SceneCore/Containers/Scene.h>
#include <SceneAPI/SceneCore/Containers/SceneManifest.h>
#include <SceneAPI/SceneCore/Events/ExportEventContext.h>
#include <SceneAPI/SceneCore/Events/ExportProductList.h>
#include <SceneAPI/SceneCore/Utilities/FileUtilities.h>
namespace AZ::SceneAPI::Behaviors
{
//
// ExportEventHandler
//
struct PrefabGroupBehavior::ExportEventHandler final
: public AZ::SceneAPI::SceneCore::ExportingComponent
{
using PreExportEventContextFunction = AZStd::function<Events::ProcessingResult(Events::PreExportEventContext&)>;
PreExportEventContextFunction m_preExportEventContextFunction;
AZ::Prefab::PrefabGroupAssetHandler m_prefabGroupAssetHandler;
ExportEventHandler() = delete;
ExportEventHandler(PreExportEventContextFunction function)
: m_preExportEventContextFunction(AZStd::move(function))
{
BindToCall(&ExportEventHandler::PrepareForExport);
AZ::SceneAPI::SceneCore::ExportingComponent::Activate();
}
~ExportEventHandler()
{
AZ::SceneAPI::SceneCore::ExportingComponent::Deactivate();
}
Events::ProcessingResult PrepareForExport(Events::PreExportEventContext& context)
{
return m_preExportEventContextFunction(context);
}
};
//
// PrefabGroupBehavior
//
void PrefabGroupBehavior::Activate()
{
m_exportEventHandler = AZStd::make_shared<ExportEventHandler>([this](auto& context)
{
return this->OnPrepareForExport(context);
});
}
void PrefabGroupBehavior::Deactivate()
{
m_exportEventHandler.reset();
}
AZStd::unique_ptr<rapidjson::Document> PrefabGroupBehavior::CreateProductAssetData(const SceneData::PrefabGroup* prefabGroup) const
{
using namespace AzToolsFramework::Prefab;
auto* prefabLoaderInterface = AZ::Interface<PrefabLoaderInterface>::Get();
if (!prefabLoaderInterface)
{
AZ_Error("prefab", false, "Could not get PrefabLoaderInterface");
return {};
}
// write to a UTF-8 string buffer
auto prefabDomRef = prefabGroup->GetPrefabDomRef();
if (!prefabDomRef)
{
AZ_Error("prefab", false, "PrefabGroup(%s) missing PrefabDom", prefabGroup->GetName().c_str());
return {};
}
const AzToolsFramework::Prefab::PrefabDom& prefabDom = prefabDomRef.value();
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer, rapidjson::UTF8<>> writer(sb);
if (prefabDom.Accept(writer) == false)
{
AZ_Error("prefab", false, "Could not write PrefabGroup(%s) to JSON", prefabGroup->GetName().c_str());
return {};
}
// validate the PrefabDom will make a valid Prefab template instance
auto templateId = prefabLoaderInterface->LoadTemplateFromString(sb.GetString(), prefabGroup->GetName().c_str());
if (templateId == InvalidTemplateId)
{
AZ_Error("prefab", false, "PrefabGroup(%s) Could not write load template", prefabGroup->GetName().c_str());
return {};
}
auto* prefabSystemComponentInterface = AZ::Interface<PrefabSystemComponentInterface>::Get();
if (!prefabSystemComponentInterface)
{
AZ_Error("prefab", false, "Could not get PrefabSystemComponentInterface");
return {};
}
// create instance to update the asset hints
auto instance = prefabSystemComponentInterface->InstantiatePrefab(templateId);
if (!instance)
{
AZ_Error("prefab", false, "PrefabGroup(%s) Could not instantiate prefab", prefabGroup->GetName().c_str());
return {};
}
auto* instanceToTemplateInterface = AZ::Interface<InstanceToTemplateInterface>::Get();
if (!instanceToTemplateInterface)
{
AZ_Error("prefab", false, "Could not get InstanceToTemplateInterface");
return {};
}
// fill out a JSON DOM
auto proceduralPrefab = AZStd::make_unique<rapidjson::Document>(rapidjson::kObjectType);
instanceToTemplateInterface->GenerateDomForInstance(*proceduralPrefab.get(), *instance.get());
return proceduralPrefab;
}
bool PrefabGroupBehavior::WriteOutProductAsset(
Events::PreExportEventContext& context,
const SceneData::PrefabGroup* prefabGroup,
const rapidjson::Document& doc) const
{
// Retrieve source asset info so we can get a string with the relative path to the asset
bool assetInfoResult;
Data::AssetInfo info;
AZStd::string watchFolder;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
assetInfoResult,
&AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath,
context.GetScene().GetSourceFilename().c_str(),
info,
watchFolder);
AZ::IO::FixedMaxPath assetPath(info.m_relativePath);
assetPath.ReplaceFilename(prefabGroup->GetName().c_str());
AZStd::string filePath = AZ::SceneAPI::Utilities::FileUtilities::CreateOutputFileName(
assetPath.c_str(),
context.GetOutputDirectory(),
AZ::Prefab::PrefabGroupAssetHandler::s_Extension);
AZ::IO::FileIOStream fileStream(filePath.c_str(), AZ::IO::OpenMode::ModeWrite);
if (fileStream.IsOpen() == false)
{
AZ_Error("prefab", false, "File path(%s) could not open for write", filePath.c_str());
return false;
}
// write to a UTF-8 string buffer
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer, rapidjson::UTF8<>> writer(sb);
if (doc.Accept(writer) == false)
{
AZ_Error("prefab", false, "PrefabGroup(%s) Could not buffer JSON", prefabGroup->GetName().c_str());
return false;
}
const auto bytesWritten = fileStream.Write(sb.GetSize(), sb.GetString());
if (bytesWritten > 1)
{
AZ::u32 subId = AZ::Crc32(filePath.c_str());
context.GetProductList().AddProduct(
filePath,
context.GetScene().GetSourceGuid(),
azrtti_typeid<Prefab::ProceduralPrefabAsset>(),
{},
AZStd::make_optional(subId));
return true;
}
return false;
}
Events::ProcessingResult PrefabGroupBehavior::OnPrepareForExport(Events::PreExportEventContext& context) const
{
AZStd::vector<const SceneData::PrefabGroup*> prefabGroupCollection;
const Containers::SceneManifest& manifest = context.GetScene().GetManifest();
for (size_t i = 0; i < manifest.GetEntryCount(); ++i)
{
const auto* group = azrtti_cast<const SceneData::PrefabGroup*>(manifest.GetValue(i).get());
if (group)
{
prefabGroupCollection.push_back(group);
}
}
if (prefabGroupCollection.empty())
{
return AZ::SceneAPI::Events::ProcessingResult::Ignored;
}
for (const auto* prefabGroup : prefabGroupCollection)
{
auto result = CreateProductAssetData(prefabGroup);
if (!result)
{
return Events::ProcessingResult::Failure;
}
if (WriteOutProductAsset(context, prefabGroup, *result.get()) == false)
{
return Events::ProcessingResult::Failure;
}
}
return Events::ProcessingResult::Success;
}
void PrefabGroupBehavior::Reflect(ReflectContext* context)
{
AZ::SceneAPI::SceneData::PrefabGroup::Reflect(context);
Prefab::ProceduralPrefabAsset::Reflect(context);
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<PrefabGroupBehavior, BehaviorComponent>()->Version(1);
}
}
}
@@ -0,0 +1,55 @@
/*
* 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 <PrefabGroup/PrefabGroup.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <SceneAPI/SceneCore/Events/ProcessingResult.h>
#include <SceneAPI/SceneCore/Components/BehaviorComponent.h>
#include <AzCore/JSON/rapidjson.h>
namespace AZ
{
class ReflectContext;
}
namespace AZ::SceneAPI::Events
{
class PreExportEventContext;
}
namespace AZ::SceneAPI::Behaviors
{
class PrefabGroupBehavior
: public SceneCore::BehaviorComponent
{
public:
AZ_COMPONENT(PrefabGroupBehavior, "{13DC2819-CAC2-4977-91D7-C870087072AB}", SceneCore::BehaviorComponent);
~PrefabGroupBehavior() override = default;
void Activate() override;
void Deactivate() override;
static void Reflect(ReflectContext* context);
private:
Events::ProcessingResult OnPrepareForExport(Events::PreExportEventContext& context) const;
AZStd::unique_ptr<rapidjson::Document> CreateProductAssetData(const SceneData::PrefabGroup* prefabGroup) const;
bool WriteOutProductAsset(
Events::PreExportEventContext& context,
const SceneData::PrefabGroup* prefabGroup,
const rapidjson::Document& doc) const;
struct ExportEventHandler;
AZStd::shared_ptr<ExportEventHandler> m_exportEventHandler;
};
}
@@ -0,0 +1,161 @@
/*
* 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 <PrefabBuilderTests.h>
#include <PrefabGroup/IPrefabGroup.h>
#include <PrefabGroup/PrefabGroup.h>
#include <AzCore/Serialization/Json/JsonSystemComponent.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzFramework/FileFunc/FileFunc.h>
namespace UnitTest
{
TEST_F(PrefabBuilderTests, PrefabGroup_FindsRequiredReflection_True)
{
using namespace AZ::SceneAPI;
auto* serializeContext = m_app.GetSerializeContext();
ASSERT_NE(nullptr, serializeContext);
SceneData::PrefabGroup::Reflect(serializeContext);
SceneData::PrefabGroup::Reflect(m_app.GetJsonRegistrationContext());
ASSERT_NE(nullptr, serializeContext->FindClassData(azrtti_typeid<DataTypes::IPrefabGroup>()));
ASSERT_NE(nullptr, serializeContext->FindClassData(azrtti_typeid<SceneData::PrefabGroup>()));
auto findElementWithName = [](const AZ::SerializeContext::ClassData* classData, const char* name)
{
auto it = AZStd::find_if(classData->m_elements.begin(), classData->m_elements.end(), [name](const auto& element)
{
return strcmp(element.m_name, name) == 0;
});
return it != classData->m_elements.end();
};
auto* prefabGroupClassData = serializeContext->FindClassData(azrtti_typeid<SceneData::PrefabGroup>());
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "name"));
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "nodeSelectionList"));
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "rules"));
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "id"));
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "prefabDomData"));
m_app.GetJsonRegistrationContext()->EnableRemoveReflection();
SceneData::PrefabGroup::Reflect(m_app.GetJsonRegistrationContext());
}
TEST_F(PrefabBuilderTests, PrefabGroup_JsonWithPrefabArbitraryPrefab_Works)
{
namespace JSR = AZ::JsonSerializationResult;
using namespace AZ::SceneAPI;
auto* serializeContext = m_app.GetSerializeContext();
ASSERT_NE(nullptr, serializeContext);
SceneData::PrefabGroup::Reflect(serializeContext);
AZ::Prefab::ProceduralPrefabAsset::Reflect(serializeContext);
SceneData::PrefabGroup::Reflect(m_app.GetJsonRegistrationContext());
AZ::Prefab::ProceduralPrefabAsset::Reflect(m_app.GetJsonRegistrationContext());
// fill out a PrefabGroup using JSON
AZStd::string_view input = R"JSON(
{
"name" : "tester",
"id" : "{49698DBC-B447-49EF-9B56-25BB29342AFB}",
"prefabDomData" : {"foo": "bar"}
})JSON";
rapidjson::Document document;
document.Parse<rapidjson::kParseCommentsFlag>(input.data(), input.size());
ASSERT_FALSE(document.HasParseError());
SceneData::PrefabGroup instancePrefabGroup;
EXPECT_EQ(AZ::JsonSerialization::Load(instancePrefabGroup, document).GetOutcome(), JSR::Outcomes::PartialDefaults);
ASSERT_TRUE(instancePrefabGroup.GetPrefabDomRef().has_value());
const AzToolsFramework::Prefab::PrefabDom& dom = instancePrefabGroup.GetPrefabDomRef().value();
EXPECT_TRUE(dom.IsObject());
EXPECT_TRUE(dom.GetObject().HasMember("foo"));
EXPECT_STREQ(dom.GetObject().FindMember("foo")->value.GetString(), "bar");
EXPECT_STREQ(instancePrefabGroup.GetName().c_str(), "tester");
EXPECT_STREQ(instancePrefabGroup.GetId().ToString<AZStd::string>().c_str(), "{49698DBC-B447-49EF-9B56-25BB29342AFB}");
m_app.GetJsonRegistrationContext()->EnableRemoveReflection();
SceneData::PrefabGroup::Reflect(m_app.GetJsonRegistrationContext());
AZ::Prefab::ProceduralPrefabAsset::Reflect(m_app.GetJsonRegistrationContext());
}
TEST_F(PrefabBuilderTests, PrefabGroup_InvalidPrefabJson_Detected)
{
using namespace AZ::SceneAPI;
AZStd::string_view input = R"JSON(
{
bad json that will not parse
})JSON";
rapidjson::Document document;
document.Parse<rapidjson::kParseCommentsFlag>(input.data(), input.size());
SceneData::PrefabGroup prefabGroup;
prefabGroup.SetId(AZ::Uuid::CreateRandom());
prefabGroup.SetName("tester");
prefabGroup.SetPrefabDom(AZStd::move(document));
const AzToolsFramework::Prefab::PrefabDom& dom = prefabGroup.GetPrefabDomRef().value();
EXPECT_TRUE(dom.IsNull());
EXPECT_STREQ("tester", prefabGroup.GetName().c_str());
}
struct PrefabBuilderBehaviorTests
: public PrefabBuilderTests
{
void SetUp() override
{
using namespace AZ::SceneAPI;
PrefabBuilderTests::SetUp();
SceneData::PrefabGroup::Reflect(m_app.GetSerializeContext());
SceneData::PrefabGroup::Reflect(m_app.GetBehaviorContext());
SceneData::PrefabGroup::Reflect(m_app.GetJsonRegistrationContext());
m_scriptContext = AZStd::make_unique<AZ::ScriptContext>();
m_scriptContext->BindTo(m_app.GetBehaviorContext());
}
void TearDown() override
{
using namespace AZ::SceneAPI;
m_app.GetJsonRegistrationContext()->EnableRemoveReflection();
SceneData::PrefabGroup::Reflect(m_app.GetJsonRegistrationContext());
m_scriptContext.reset();
PrefabBuilderTests::TearDown();
}
void ExpectExecute(AZStd::string_view script)
{
EXPECT_TRUE(m_scriptContext->Execute(script.data()));
}
AZStd::unique_ptr<AZ::ScriptContext> m_scriptContext;
};
TEST_F(PrefabBuilderBehaviorTests, PrefabGroup_PrefabGroupClass_Exists)
{
ExpectExecute("group = PrefabGroup()");
ExpectExecute("assert(group)");
ExpectExecute("assert(group.name)");
ExpectExecute("assert(group.id)");
ExpectExecute("assert(group.prefabDomData)");
}
TEST_F(PrefabBuilderBehaviorTests, PrefabGroup_PrefabGroupAssignment_Works)
{
ExpectExecute("group = PrefabGroup()");
ExpectExecute("group.name = 'tester'");
ExpectExecute("group.id = Uuid.CreateString('{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}', 0)");
ExpectExecute("group.prefabDomData = '{\"foo\": \"bar\"}'");
ExpectExecute("assert(group.name == 'tester')");
ExpectExecute("assert(tostring(group.id) == '{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}')");
ExpectExecute("assert(group.prefabDomData == '{\\n \"foo\": \"bar\"\\n}')");
}
}
@@ -0,0 +1,185 @@
/*
* 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 <PrefabGroup/ProceduralAssetHandler.h>
#include <AzCore/Asset/AssetTypeInfoBus.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/FileFunc/FileFunc.h>
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
#include <AzToolsFramework/Prefab/PrefabLoaderInterface.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
namespace AZ::Prefab
{
// AssetTypeInfoHandler
class PrefabGroupAssetHandler::AssetTypeInfoHandler final
: public AZ::AssetTypeInfoBus::Handler
{
public:
AZ_CLASS_ALLOCATOR(AssetTypeInfoHandler, AZ::SystemAllocator, 0);
AssetTypeInfoHandler();
~AssetTypeInfoHandler() override;
AZ::Data::AssetType GetAssetType() const override;
const char* GetAssetTypeDisplayName() const override;
const char* GetGroup() const override;
const char* GetBrowserIcon() const override;
void GetAssetTypeExtensions(AZStd::vector<AZStd::string>& extensions) override;
};
PrefabGroupAssetHandler::AssetTypeInfoHandler::AssetTypeInfoHandler()
{
AZ::AssetTypeInfoBus::Handler::BusConnect(azrtti_typeid<ProceduralPrefabAsset>());
}
PrefabGroupAssetHandler::AssetTypeInfoHandler::~AssetTypeInfoHandler()
{
AZ::AssetTypeInfoBus::Handler::BusDisconnect(azrtti_typeid<ProceduralPrefabAsset>());
}
AZ::Data::AssetType PrefabGroupAssetHandler::AssetTypeInfoHandler::GetAssetType() const
{
return azrtti_typeid<ProceduralPrefabAsset>();
}
const char* PrefabGroupAssetHandler::AssetTypeInfoHandler::GetAssetTypeDisplayName() const
{
return "Procedural Prefab";
}
const char* PrefabGroupAssetHandler::AssetTypeInfoHandler::GetGroup() const
{
return "Prefab";
}
const char* PrefabGroupAssetHandler::AssetTypeInfoHandler::GetBrowserIcon() const
{
return "Icons/Components/Box.png";
}
void PrefabGroupAssetHandler::AssetTypeInfoHandler::GetAssetTypeExtensions(AZStd::vector<AZStd::string>& extensions)
{
extensions.push_back(PrefabGroupAssetHandler::s_Extension);
}
// PrefabGroupAssetHandler
AZStd::string_view PrefabGroupAssetHandler::s_Extension{ "procprefab" };
PrefabGroupAssetHandler::PrefabGroupAssetHandler()
{
auto assetCatalog = AZ::Data::AssetCatalogRequestBus::FindFirstHandler();
if (assetCatalog)
{
assetCatalog->EnableCatalogForAsset(azrtti_typeid<ProceduralPrefabAsset>());
assetCatalog->AddExtension(s_Extension.data());
}
if (AZ::Data::AssetManager::IsReady())
{
AZ::Data::AssetManager::Instance().RegisterHandler(this, azrtti_typeid<ProceduralPrefabAsset>());
}
m_assetTypeInfoHandler = AZStd::make_shared<AssetTypeInfoHandler>();
}
PrefabGroupAssetHandler::~PrefabGroupAssetHandler()
{
m_assetTypeInfoHandler.reset();
if (AZ::Data::AssetManager::IsReady())
{
AZ::Data::AssetManager::Instance().UnregisterHandler(this);
}
}
AZ::Data::AssetData* PrefabGroupAssetHandler::CreateAsset([[maybe_unused]] const AZ::Data::AssetId& id, const AZ::Data::AssetType& type)
{
if (type != azrtti_typeid<ProceduralPrefabAsset>())
{
AZ_Error("prefab", false, "Invalid asset type! Only handle 'ProceduralPrefabAsset'");
return nullptr;
}
return aznew ProceduralPrefabAsset{};
}
void PrefabGroupAssetHandler::DestroyAsset(AZ::Data::AssetData* ptr)
{
// Note: the PrefabLoaderInterface will handle the lifetime of the Prefab Template
delete ptr;
}
void PrefabGroupAssetHandler::GetHandledAssetTypes(AZStd::vector<AZ::Data::AssetType>& assetTypes)
{
assetTypes.push_back(azrtti_typeid<ProceduralPrefabAsset>());
}
AZ::Data::AssetHandler::LoadResult PrefabGroupAssetHandler::LoadAssetData(
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
[[maybe_unused]] const AZ::Data::AssetFilterCB& assetLoadFilterCB)
{
using namespace AzToolsFramework::Prefab;
auto* proceduralPrefabAsset = asset.GetAs<ProceduralPrefabAsset>();
if (!proceduralPrefabAsset)
{
AZ_Error("prefab", false, "This should be a ProceduralPrefabAsset type, as this is the only type we process!");
return LoadResult::Error;
}
AZStd::string buffer;
buffer.resize(stream->GetLoadedSize());
stream->Read(stream->GetLoadedSize(), buffer.data());
auto jsonOutcome = AZ::JsonSerializationUtils::ReadJsonString(buffer);
if (jsonOutcome.IsSuccess() == false)
{
AZ_Error("prefab", false, "Asset JSON failed to compile %s", jsonOutcome.GetError().c_str());
return LoadResult::Error;
}
const auto& jsonDoc = jsonOutcome.GetValue();
if (jsonDoc.IsObject() == false)
{
return LoadResult::Error;
}
if (jsonDoc.FindMember("Source") == jsonDoc.MemberEnd())
{
return LoadResult::Error;
}
const auto& templateName = jsonDoc["Source"];
AZStd::string stringJson;
auto stringOutcome = AZ::JsonSerializationUtils::WriteJsonString(jsonDoc, stringJson);
if (stringOutcome.IsSuccess() == false)
{
AZ_Error("prefab", false, "Could not write to JSON string %s", stringOutcome.GetError().c_str());
return LoadResult::Error;
}
// prepare the template
auto* prefabLoaderInterface = AZ::Interface<PrefabLoaderInterface>::Get();
if (!prefabLoaderInterface)
{
return LoadResult::Error;
}
auto templateId = prefabLoaderInterface->LoadTemplateFromString(stringJson.data(), templateName.GetString());
if (templateId == InvalidTemplateId)
{
return LoadResult::Error;
}
proceduralPrefabAsset->SetTemplateId(templateId);
proceduralPrefabAsset->SetTemplateName(templateName.GetString());
return LoadResult::LoadComplete;
}
AZStd::unique_ptr<PrefabGroupAssetHandler> s_PrefabGroupAssetHandler;
}
@@ -0,0 +1,39 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AzCore/std/containers/vector.h>
namespace AZ::Prefab
{
class PrefabGroupAssetHandler final
: public AZ::Data::AssetHandler
{
public:
AZ_CLASS_ALLOCATOR(PrefabGroupAssetHandler, AZ::SystemAllocator, 0);
PrefabGroupAssetHandler();
~PrefabGroupAssetHandler() override;
static AZStd::string_view s_Extension;
protected:
AZ::Data::AssetData* CreateAsset(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type) override;
void DestroyAsset(AZ::Data::AssetData* ptr) override;
void GetHandledAssetTypes(AZStd::vector<AZ::Data::AssetType>& assetTypes) override;
AZ::Data::AssetHandler::LoadResult LoadAssetData(
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
const AZ::Data::AssetFilterCB& assetLoadFilterCB) override;
class AssetTypeInfoHandler;
AZStd::shared_ptr<AssetTypeInfoHandler> m_assetTypeInfoHandler;
};
}
@@ -0,0 +1,162 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <PrefabGroup/PrefabBuilderTests.h>
#include <PrefabGroup/IPrefabGroup.h>
#include <PrefabGroup/PrefabGroup.h>
#include <AzCore/Serialization/Json/JsonSystemComponent.h>
namespace UnitTest
{
TEST_F(PrefabBuilderTests, PrefabGroup_FindsRequiredReflection_True)
{
using namespace AZ::SceneAPI;
auto* serializeContext = m_app.GetSerializeContext();
ASSERT_NE(nullptr, serializeContext);
SceneData::PrefabGroup::Reflect(serializeContext);
ASSERT_NE(nullptr, serializeContext->FindClassData(azrtti_typeid<DataTypes::IPrefabGroup>()));
ASSERT_NE(nullptr, serializeContext->FindClassData(azrtti_typeid<SceneData::PrefabGroup>()));
auto findElementWithName = [](const AZ::SerializeContext::ClassData* classData, const char* name)
{
auto it = AZStd::find_if(classData->m_elements.begin(), classData->m_elements.end(), [name](const auto& element)
{
return strcmp(element.m_name, name) == 0;
});
return it != classData->m_elements.end();
};
auto* prefabGroupClassData = serializeContext->FindClassData(azrtti_typeid<SceneData::PrefabGroup>());
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "name"));
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "nodeSelectionList"));
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "rules"));
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "id"));
EXPECT_TRUE(findElementWithName(prefabGroupClassData, "prefabDomBuffer"));
}
TEST_F(PrefabBuilderTests, PrefabGroup_JsonWithPrefabArbitraryPrefab_Works)
{
using namespace AZ::SceneAPI;
auto* serializeContext = m_app.GetSerializeContext();
ASSERT_NE(nullptr, serializeContext);
SceneData::PrefabGroup::Reflect(serializeContext);
// fill out a PrefabGroup using JSON
AZStd::string_view input = R"JSON(
{
"name" : "tester",
"id" : "{49698DBC-B447-49EF-9B56-25BB29342AFB}",
"prefabDomBuffer" : "{\"foo\":\"bar\"}"
})JSON";
rapidjson::Document document;
document.Parse<rapidjson::kParseCommentsFlag>(input.data(), input.size());
ASSERT_FALSE(document.HasParseError());
SceneData::PrefabGroup instancePrefabGroup;
AZ::JsonSerialization::Load(instancePrefabGroup, document);
const auto& dom = instancePrefabGroup.GetPrefabDom();
EXPECT_TRUE(dom.GetObject().HasMember("foo"));
EXPECT_STREQ(dom.GetObject().FindMember("foo")->value.GetString(), "bar");
EXPECT_STREQ(instancePrefabGroup.GetName().c_str(), "tester");
EXPECT_STREQ(
instancePrefabGroup.GetId().ToString<AZStd::string>().c_str(),
"{49698DBC-B447-49EF-9B56-25BB29342AFB}");
EXPECT_TRUE(instancePrefabGroup.GetPrefabDom().IsObject());
}
TEST_F(PrefabBuilderTests, PrefabGroup_InvalidPrefabJson_Detected)
{
using namespace AZ::SceneAPI;
AZStd::string_view input = R"JSON(
{
bad json that will not parse
})JSON";
rapidjson::Document document;
document.Parse<rapidjson::kParseCommentsFlag>(input.data(), input.size());
SceneData::PrefabGroup prefabGroup;
prefabGroup.SetId(AZStd::move(AZ::Uuid::CreateRandom()));
prefabGroup.SetName(AZStd::move("tester"));
prefabGroup.SetPrefabDom(AZStd::move(document));
const auto& dom = prefabGroup.GetPrefabDom();
EXPECT_TRUE(dom.IsNull());
EXPECT_STREQ("tester", prefabGroup.GetName().c_str());
}
TEST_F(PrefabBuilderTests, PrefabGroup_InvalidPrefabJsonBuffer_Detected)
{
using namespace AZ::SceneAPI;
AZStd::string_view inputJson = R"JSON(
{
bad json that will not parse
})JSON";
SceneData::PrefabGroup prefabGroup;
prefabGroup.SetId(AZStd::move(AZ::Uuid::CreateRandom()));
prefabGroup.SetName(AZStd::move("tester"));
prefabGroup.SetPrefabDomBuffer(std::move(inputJson));
const auto& dom = prefabGroup.GetPrefabDom();
EXPECT_TRUE(dom.IsNull());
EXPECT_STREQ("tester", prefabGroup.GetName().c_str());
}
struct PrefabBuilderBehaviorTests
: public PrefabBuilderTests
{
void SetUp() override
{
using namespace AZ::SceneAPI;
PrefabBuilderTests::SetUp();
SceneData::PrefabGroup::Reflect(m_app.GetSerializeContext());
SceneData::PrefabGroup::Reflect(m_app.GetBehaviorContext());
m_scriptContext = AZStd::make_unique<AZ::ScriptContext>();
m_scriptContext->BindTo(m_app.GetBehaviorContext());
}
void TearDown() override
{
m_scriptContext.reset();
PrefabBuilderTests::TearDown();
}
void ExpectExecute(AZStd::string_view script)
{
EXPECT_TRUE(m_scriptContext->Execute(script.data()));
}
AZStd::unique_ptr<AZ::ScriptContext> m_scriptContext;
};
TEST_F(PrefabBuilderBehaviorTests, PrefabGroup_PrefabGroupClass_Exists)
{
ExpectExecute("group = PrefabGroup()");
ExpectExecute("assert(group)");
ExpectExecute("assert(group.name)");
ExpectExecute("assert(group.id)");
ExpectExecute("assert(group.prefabDomBuffer)");
}
TEST_F(PrefabBuilderBehaviorTests, PrefabGroup_PrefabGroupAssignment_Works)
{
ExpectExecute("group = PrefabGroup()");
ExpectExecute("group.name = 'tester'");
ExpectExecute("group.id = Uuid.CreateString('{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}', 0)");
ExpectExecute("group.prefabDomBuffer = '{}'");
ExpectExecute("assert(group.name == 'tester')");
ExpectExecute("assert(tostring(group.id) == '{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}')");
ExpectExecute("assert(group.prefabDomBuffer == '{}')");
}
}
@@ -9,4 +9,11 @@
set(FILES
PrefabBuilderComponent.h
PrefabBuilderComponent.cpp
PrefabGroup/IPrefabGroup.h
PrefabGroup/PrefabGroup.cpp
PrefabGroup/PrefabGroup.h
PrefabGroup/PrefabGroupBehavior.cpp
PrefabGroup/PrefabGroupBehavior.h
PrefabGroup/ProceduralAssetHandler.cpp
PrefabGroup/ProceduralAssetHandler.h
)
@@ -9,4 +9,7 @@
set(FILES
PrefabBuilderTests.h
PrefabBuilderTests.cpp
PrefabGroup/PrefabGroupTests.cpp
PrefabGroup/PrefabBehaviorTests.cpp
PrefabGroup/PrefabBehaviorTests.inl
)
@@ -104,6 +104,15 @@ class SceneManifest():
self.manifest['values'].append(meshGroup)
return meshGroup
def add_prefab_group(self, name, id, json) -> dict:
prefabGroup = {}
prefabGroup['$type'] = '{99FE3C6F-5B55-4D8B-8013-2708010EC715} PrefabGroup'
prefabGroup['name'] = name
prefabGroup['id'] = id
prefabGroup['prefabDomData'] = json
self.manifest['values'].append(prefabGroup)
return prefabGroup
def mesh_group_select_node(self, meshGroup, nodeName):
meshGroup['nodeSelectionList']['selectedNodes'].append(nodeName)
@@ -13,6 +13,7 @@
#include <SceneAPI/SceneCore/DataTypes/IGraphObject.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/containers/set.h>
#include <AzCore/Utils/Utils.h>
#include <AzFramework/Application/Application.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/Debug/TraceContext.h>
@@ -34,6 +35,9 @@
#include <SceneAPI/SceneCore/DataTypes/GraphData/IAnimationData.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <rapidjson/pointer.h>
#include <SceneBuilder/SceneBuilderWorker.h>
#include <SceneBuilder/TraceMessageHook.h>
@@ -81,6 +85,93 @@ namespace SceneBuilder
return m_cachedFingerprint.c_str();
}
void SceneBuilderWorker::PopulateSourceDependencies(const AZStd::string& manifestJson, AZStd::vector<AssetBuilderSDK::SourceFileDependency>& sourceFileDependencies)
{
auto readJsonOutcome = AZ::JsonSerializationUtils::ReadJsonString(manifestJson);
AZStd::string errorMsg;
if (!readJsonOutcome.IsSuccess())
{
// This may be an old format xml file. We don't have any dependencies in the old format so there's no point trying to parse an xml
return;
}
rapidjson::Document document = readJsonOutcome.TakeValue();
auto manifestObject = document.GetObject();
auto valuesIterator = manifestObject.FindMember("values");
auto valuesArray = valuesIterator->value.GetArray();
AZStd::vector<AZStd::string> paths;
AZ::SceneAPI::Events::AssetImportRequestBus::Broadcast(
&AZ::SceneAPI::Events::AssetImportRequestBus::Events::GetManifestDependencyPaths, paths);
for (const auto& value : valuesArray)
{
auto object = value.GetObject();
for (const auto& path : paths)
{
rapidjson::Pointer pointer(path.c_str());
auto dependencyValue = pointer.Get(object);
if (dependencyValue && dependencyValue->IsString())
{
AZStd::string dependency = dependencyValue->GetString();
sourceFileDependencies.emplace_back(AssetBuilderSDK::SourceFileDependency(
dependency, AZ::Uuid::CreateNull(), AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Absolute));
}
}
}
}
bool SceneBuilderWorker::ManifestDependencyCheck(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
{
AZStd::string manifestExtension;
AZStd::string generatedManifestExtension;
AZ::SceneAPI::Events::AssetImportRequestBus::Broadcast(
&AZ::SceneAPI::Events::AssetImportRequestBus::Events::GetManifestExtension, manifestExtension);
AZ::SceneAPI::Events::AssetImportRequestBus::Broadcast(
&AZ::SceneAPI::Events::AssetImportRequestBus::Events::GetGeneratedManifestExtension, generatedManifestExtension);
if (manifestExtension.empty() || generatedManifestExtension.empty())
{
AZ_Error("SceneBuilderWorker", false, "Failed to get scene manifest extension");
return false;
}
AZ::SettingsRegistryInterface::FixedValueString assetCacheRoot;
AZ::SettingsRegistry::Get()->Get(assetCacheRoot, AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder);
auto manifestPath = (AZ::IO::Path(request.m_watchFolder) / (request.m_sourceFile + manifestExtension));
auto generatedManifestPath = (AZ::IO::Path(assetCacheRoot) / (request.m_sourceFile + generatedManifestExtension));
auto populateDependenciesFunc = [&response](const AZStd::string& path)
{
auto readFileOutcome = AZ::Utils::ReadFile(path, AZ::SceneAPI::Containers::SceneManifest::MaxSceneManifestFileSizeInBytes);
if (!readFileOutcome.IsSuccess())
{
AZ_Error("SceneBuilderWorker", false, "%s", readFileOutcome.GetError().c_str());
return;
}
PopulateSourceDependencies(readFileOutcome.TakeValue(), response.m_sourceFileDependencyList);
};
if (AZ::IO::FileIOBase::GetInstance()->Exists(manifestPath.Native().c_str()))
{
populateDependenciesFunc(manifestPath.Native());
}
else if (AZ::IO::FileIOBase::GetInstance()->Exists(generatedManifestPath.Native().c_str()))
{
populateDependenciesFunc(generatedManifestPath.Native());
}
return true;
}
void SceneBuilderWorker::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
{
// Check for shutdown
@@ -118,6 +209,11 @@ namespace SceneBuilder
sourceFileDependencyInfo.m_sourceDependencyType = AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards;
response.m_sourceFileDependencyList.push_back(sourceFileDependencyInfo);
if (!ManifestDependencyCheck(request, response))
{
return;
}
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
}
@@ -11,6 +11,7 @@
#include <AzCore/Component/Component.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AssetBuilderSDK/AssetBuilderBusses.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
namespace AssetBuilderSDK
{
@@ -45,11 +46,15 @@ namespace SceneBuilder
public:
~SceneBuilderWorker() override = default;
void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response);
void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response);
void ShutDown() override;
const char* GetFingerprint() const;
static void PopulateSourceDependencies(
const AZStd::string& manifestJson, AZStd::vector<AssetBuilderSDK::SourceFileDependency>& sourceFileDependencies);
static bool ManifestDependencyCheck(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response);
static AZ::Uuid GetUUID();
void PopulateProductDependencies(const AZ::SceneAPI::Events::ExportProduct& exportProduct, const char* watchFolder, AssetBuilderSDK::JobProduct& jobProduct) const;
@@ -11,11 +11,14 @@
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/UnitTest/Mocks/MockFileIOBase.h>
#include <AzCore/UserSettings/UserSettingsComponent.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/Application/ToolsApplication.h>
#include <SceneAPI/SceneCore/Events/AssetImportRequest.h>
#include <SceneBuilder/SceneBuilderWorker.h>
#include <SceneAPI/SceneCore/Events/ExportProductList.h>
#include <Tests/FileIOBaseTestTypes.h>
using namespace AZ;
using namespace SceneBuilder;
@@ -193,3 +196,200 @@ TEST_F(SceneBuilderTests, SceneBuilderWorker_ExportProductDependencies_ProductAn
TestSuccessCase(exportProduct, expectedPathDependencies, { dependencyId });
}
struct ImportHandler
: SceneAPI::Events::AssetImportRequestBus::Handler
{
ImportHandler()
{
BusConnect();
}
~ImportHandler() override
{
BusDisconnect();
}
void GetManifestDependencyPaths(AZStd::vector<AZStd::string>& paths) override
{
paths.emplace_back("/scriptFilename");
paths.emplace_back("/layer1/layer2/0/target");
}
void GetManifestExtension(AZStd::string& result) override
{
result = ".test";
}
void GetGeneratedManifestExtension(AZStd::string& result) override
{
result = ".test.gen";
}
};
using SourceDependencyTests = UnitTest::ScopedAllocatorSetupFixture;
namespace SourceDependencyJson
{
constexpr const char* TestJson = R"JSON(
{
"values": [
{
"$type": "Test1",
"scriptFilename": "a/test/path.png"
},
{
"$type": "Test2",
"layer1" : {
"layer2" : [
{
"target": "value.png",
"otherData": "value2.png"
},
{
"target" : "wrong.png"
}
]
}
}
]
}
)JSON";
}
TEST_F(SourceDependencyTests, SourceDependencyTest)
{
ImportHandler handler;
AZStd::vector<AssetBuilderSDK::SourceFileDependency> dependencies;
SceneBuilderWorker::PopulateSourceDependencies(SourceDependencyJson::TestJson, dependencies);
ASSERT_EQ(dependencies.size(), 2);
ASSERT_STREQ(dependencies[0].m_sourceFileDependencyPath.c_str(), "a/test/path.png");
ASSERT_STREQ(dependencies[1].m_sourceFileDependencyPath.c_str(), "value.png");
}
struct SettingsRegistryMock : AZ::Interface<SettingsRegistryInterface>::Registrar
{
bool Get(FixedValueString& result, AZStd::string_view) const override
{
result = "cache";
return true;
}
void SetApplyPatchSettings(const AZ::JsonApplyPatchSettings& /*applyPatchSettings*/) override{}
void GetApplyPatchSettings(AZ::JsonApplyPatchSettings& /*applyPatchSettings*/) override{}
MOCK_CONST_METHOD1(GetType, Type (AZStd::string_view));
MOCK_CONST_METHOD2(Visit, bool (Visitor&, AZStd::string_view));
MOCK_CONST_METHOD2(Visit, bool (const VisitorCallback&, AZStd::string_view));
MOCK_METHOD1(RegisterNotifier, NotifyEventHandler (const NotifyCallback&));
MOCK_METHOD1(RegisterNotifier, NotifyEventHandler (NotifyCallback&&));
MOCK_METHOD1(RegisterPreMergeEvent, PreMergeEventHandler (const PreMergeEventCallback&));
MOCK_METHOD1(RegisterPreMergeEvent, PreMergeEventHandler (PreMergeEventCallback&&));
MOCK_METHOD1(RegisterPostMergeEvent, PostMergeEventHandler (const PostMergeEventCallback&));
MOCK_METHOD1(RegisterPostMergeEvent, PostMergeEventHandler (PostMergeEventCallback&&));
MOCK_CONST_METHOD2(Get, bool (bool&, AZStd::string_view));
MOCK_CONST_METHOD2(Get, bool (s64&, AZStd::string_view));
MOCK_CONST_METHOD2(Get, bool (u64&, AZStd::string_view));
MOCK_CONST_METHOD2(Get, bool (double&, AZStd::string_view));
MOCK_CONST_METHOD2(Get, bool (AZStd::string&, AZStd::string_view));
MOCK_CONST_METHOD3(GetObject, bool (void*, Uuid, AZStd::string_view));
MOCK_METHOD2(Set, bool (AZStd::string_view, bool));
MOCK_METHOD2(Set, bool (AZStd::string_view, s64));
MOCK_METHOD2(Set, bool (AZStd::string_view, u64));
MOCK_METHOD2(Set, bool (AZStd::string_view, double));
MOCK_METHOD2(Set, bool (AZStd::string_view, AZStd::string_view));
MOCK_METHOD2(Set, bool (AZStd::string_view, const char*));
MOCK_METHOD3(SetObject, bool (AZStd::string_view, const void*, Uuid));
MOCK_METHOD1(Remove, bool (AZStd::string_view));
MOCK_METHOD3(MergeCommandLineArgument, bool (AZStd::string_view, AZStd::string_view, const CommandLineArgumentSettings&));
MOCK_METHOD2(MergeSettings, bool (AZStd::string_view, Format));
MOCK_METHOD4(MergeSettingsFile, bool (AZStd::string_view, Format, AZStd::string_view, AZStd::vector<char>*));
MOCK_METHOD5(MergeSettingsFolder, bool (AZStd::string_view, const Specializations&, AZStd::string_view, AZStd::string_view, AZStd::vector<char>*));
MOCK_METHOD1(SetUseFileIO, void (bool));
};
struct SourceDependencyMockedIOTests : UnitTest::ScopedAllocatorSetupFixture
, UnitTest::SetRestoreFileIOBaseRAII
{
SourceDependencyMockedIOTests()
: UnitTest::SetRestoreFileIOBaseRAII(m_ioMock)
{
}
void SetUp() override
{
using namespace ::testing;
ON_CALL(m_ioMock, Open(_, _, _))
.WillByDefault(Invoke(
[](auto, auto, IO::HandleType& handle)
{
handle = 1234;
return AZ::IO::Result(AZ::IO::ResultCode::Success);
}));
ON_CALL(m_ioMock, Size(An<AZ::IO::HandleType>(), _)).WillByDefault(Invoke([](auto, AZ::u64& size)
{
size = strlen(SourceDependencyJson::TestJson);
return AZ::IO::ResultCode::Success;
}));
EXPECT_CALL(m_ioMock, Read(_, _, _, _, _))
.WillRepeatedly(Invoke(
[](auto, void* buffer, auto, auto, AZ::u64* bytesRead)
{
memcpy(buffer, SourceDependencyJson::TestJson, strlen(SourceDependencyJson::TestJson));
*bytesRead = strlen(SourceDependencyJson::TestJson);
return AZ::IO::ResultCode::Success;
}));
EXPECT_CALL(m_ioMock, Close(_)).WillRepeatedly(Return(AZ::IO::ResultCode::Success));
}
IO::NiceFileIOBaseMock m_ioMock;
};
TEST_F(SourceDependencyMockedIOTests, RegularManifestHasPriority)
{
ImportHandler handler;
SettingsRegistryMock settingsRegistry;
AssetBuilderSDK::CreateJobsRequest request;
AssetBuilderSDK::CreateJobsResponse response;
request.m_sourceFile = "file.fbx";
using namespace ::testing;
AZStd::string genPath = AZStd::string("cache").append(1, AZ_TRAIT_OS_PATH_SEPARATOR).append("file.fbx.test.gen");
EXPECT_CALL(m_ioMock, Exists(StrEq("file.fbx.test"))).WillRepeatedly(Return(true));
EXPECT_CALL(m_ioMock, Exists(StrEq(genPath.c_str()))).Times(Exactly(0));
ASSERT_TRUE(SceneBuilderWorker::ManifestDependencyCheck(request, response));
ASSERT_EQ(response.m_sourceFileDependencyList.size(), 2);
}
TEST_F(SourceDependencyMockedIOTests, GeneratedManifestTest)
{
ImportHandler handler;
SettingsRegistryMock settingsRegistry;
AssetBuilderSDK::CreateJobsRequest request;
AssetBuilderSDK::CreateJobsResponse response;
request.m_sourceFile = "file.fbx";
using namespace ::testing;
AZStd::string genPath = AZStd::string("cache").append(1, AZ_TRAIT_OS_PATH_SEPARATOR).append("file.fbx.test.gen");
EXPECT_CALL(m_ioMock, Exists(StrEq("file.fbx.test"))).WillRepeatedly(Return(false));
EXPECT_CALL(m_ioMock, Exists(StrEq(genPath.c_str()))).WillRepeatedly(Return(true));
ASSERT_TRUE(SceneBuilderWorker::ManifestDependencyCheck(request, response));
ASSERT_EQ(response.m_sourceFileDependencyList.size(), 2);
}