diff --git a/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py b/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py new file mode 100644 index 0000000000..d5a7acfe91 --- /dev/null +++ b/AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py @@ -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 diff --git a/AutomatedTesting/Gem/Code/enabled_gems.cmake b/AutomatedTesting/Gem/Code/enabled_gems.cmake index 0d281661b9..30740a489d 100644 --- a/AutomatedTesting/Gem/Code/enabled_gems.cmake +++ b/AutomatedTesting/Gem/Code/enabled_gems.cmake @@ -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 ) diff --git a/AutomatedTesting/multiple_mesh_one_material/FBXTestTexture.png b/AutomatedTesting/multiple_mesh_one_material/FBXTestTexture.png new file mode 100644 index 0000000000..4f52364cb3 --- /dev/null +++ b/AutomatedTesting/multiple_mesh_one_material/FBXTestTexture.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:827a63985273229050bf4f63030bcc666f045091fe81cf8157a9ca23b40074b6 +size 3214 diff --git a/AutomatedTesting/multiple_mesh_one_material/multiple_mesh_one_material.fbx b/AutomatedTesting/multiple_mesh_one_material/multiple_mesh_one_material.fbx new file mode 100644 index 0000000000..d9deb899f0 --- /dev/null +++ b/AutomatedTesting/multiple_mesh_one_material/multiple_mesh_one_material.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8d24963e6e8765205bc79cbe2304fc39f1245ee75249e2834a71c96c3cab824 +size 22700 diff --git a/AutomatedTesting/multiple_mesh_one_material/multiple_mesh_one_material.fbx.assetinfo b/AutomatedTesting/multiple_mesh_one_material/multiple_mesh_one_material.fbx.assetinfo new file mode 100644 index 0000000000..fe18a16cb5 --- /dev/null +++ b/AutomatedTesting/multiple_mesh_one_material/multiple_mesh_one_material.fbx.assetinfo @@ -0,0 +1,8 @@ +{ + "values": [ + { + "$type": "ScriptProcessorRule", + "scriptFilename": "Editor/Scripts/scene_mesh_to_prefab.py" + } + ] +} \ No newline at end of file diff --git a/Code/Framework/AzCore/AzCore/Math/Uuid.cpp b/Code/Framework/AzCore/AzCore/Math/Uuid.cpp index d875e28a1e..410d235a37 100644 --- a/Code/Framework/AzCore/AzCore/Math/Uuid.cpp +++ b/Code/Framework/AzCore/AzCore/Math/Uuid.cpp @@ -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) diff --git a/Code/Framework/AzCore/AzCore/Math/Uuid.h b/Code/Framework/AzCore/AzCore/Math/Uuid.h index ea920e45c8..6b77e7ec3e 100644 --- a/Code/Framework/AzCore/AzCore/Math/Uuid.h +++ b/Code/Framework/AzCore/AzCore/Math/Uuid.h @@ -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); } diff --git a/Code/Framework/AzCore/Tests/UUIDTests.cpp b/Code/Framework/AzCore/Tests/UUIDTests.cpp index a18dc33c4f..64b9048a62 100644 --- a/Code/Framework/AzCore/Tests/UUIDTests.cpp +++ b/Code/Framework/AzCore/Tests/UUIDTests.cpp @@ -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); + } } diff --git a/Code/Framework/AzFramework/AzFramework/Entity/BehaviorEntity.cpp b/Code/Framework/AzFramework/AzFramework/Entity/BehaviorEntity.cpp index 725f71a3c9..5d82cf488a 100644 --- a/Code/Framework/AzFramework/AzFramework/Entity/BehaviorEntity.cpp +++ b/Code/Framework/AzFramework/AzFramework/Entity/BehaviorEntity.cpp @@ -133,6 +133,8 @@ namespace AzFramework behaviorContext->Class("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() ->Constructor() diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp index c54735fe91..fbd066ec6e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp @@ -69,6 +69,7 @@ #include #include #include +#include #include AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") // 4251: 'QFileInfo::d_ptr': class 'QSharedDataPointer' needs to have dll-interface to be used by clients of class 'QFileInfo' @@ -271,7 +272,8 @@ namespace AzToolsFramework azrtti_typeid(), azrtti_typeid(), azrtti_typeid(), - azrtti_typeid() + azrtti_typeid(), + azrtti_typeid() }); return components; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AzToolsFrameworkModule.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AzToolsFrameworkModule.cpp index 05d67d9d71..d2a88df544 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AzToolsFrameworkModule.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AzToolsFrameworkModule.cpp @@ -53,6 +53,7 @@ #include #include #include +#include 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(), diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EntityUtilityComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EntityUtilityComponent.cpp new file mode 100644 index 0000000000..7fb998f012 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EntityUtilityComponent.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AzToolsFramework +{ + void ComponentDetails::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Field("TypeInfo", &ComponentDetails::m_typeInfo) + ->Field("BaseClasses", &ComponentDetails::m_baseClasses); + + serializeContext->RegisterGenericType>(); + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class() + ->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().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(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().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 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 EntityUtilityComponent::FindMatchingComponents(const AZStd::string& searchTerm) + { + AZ::SerializeContext* serializeContext = nullptr; + AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext); + + if (m_typeInfo.empty()) + { + serializeContext->EnumerateDerived( + [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{}); + + 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 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().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(context)) + { + serializeContext->Class(); + } + + if (auto* behaviorContext = azrtti_cast(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") + ->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(UtilityEntityContextId); + m_entityContext->InitContext(); + EntityUtilityBus::Handler::BusConnect(); + } + + void EntityUtilityComponent::Deactivate() + { + EntityUtilityBus::Handler::BusDisconnect(); + m_entityContext = nullptr; + } +} diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EntityUtilityComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EntityUtilityComponent.h new file mode 100644 index 0000000000..af7c353163 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EntityUtilityComponent.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 FindMatchingComponents(const AZStd::string& searchTerm) = 0; + + virtual void ResetEntityContext() = 0; + }; + + using EntityUtilityBus = AZ::EBus; + + 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 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 m_entityContext; + + // TypeId, TypeName, Vector + AZStd::vector>> m_typeInfo; + + // Keep track of the entities we create so they can be reset + AZStd::vector m_createdEntities; + }; +}; // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/FocusMode/FocusModeInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/FocusMode/FocusModeInterface.h index 75e3bab60f..5455e8d773 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/FocusMode/FocusModeInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/FocusMode/FocusModeInterface.h @@ -10,6 +10,7 @@ #include #include +#include #include #include diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp index 09439724cd..07bda45c8a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp @@ -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::Register(this); + m_scriptingPrefabLoader.Connect(this); } void PrefabLoader::UnregisterPrefabLoaderInterface() { + m_scriptingPrefabLoader.Disconnect(); AZ::Interface::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; } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.h index 15f201e027..da2c3f3161 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.h @@ -15,6 +15,7 @@ #include #include #include +#include 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> StoreTemplateIntoFileFormat(TemplateId templateId); PrefabSystemComponentInterface* m_prefabSystemComponentInterface = nullptr; + ScriptingPrefabLoader m_scriptingPrefabLoader; AZ::IO::Path m_projectPathWithOsSeparator; AZ::IO::Path m_projectPathWithSlashSeparator; }; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderInterface.h index a428f8a7b9..b4586ad5bf 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderInterface.h @@ -99,7 +99,6 @@ namespace AzToolsFramework // Generates a new path static AZ::IO::Path GeneratePath(); }; - } // namespace Prefab } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderScriptingBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderScriptingBus.h new file mode 100644 index 0000000000..6e9d64b147 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderScriptingBus.h @@ -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 +#include +#include +#include +#include + +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 SaveTemplateToString(TemplateId templateId) = 0; + }; + + using PrefabLoaderScriptingBus = AZ::EBus; + + } // namespace Prefab +} // namespace AzToolsFramework + diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp index 896e40d929..b1fdf9784d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -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(context); - if (serialize) + if (AZ::SerializeContext* serialize = azrtti_cast(context)) { serialize->Class()->Version(1); } + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + + behaviorContext->EBus("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(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); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.h index f640eb2f1b..480bb83121 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.h @@ -27,6 +27,7 @@ #include #include #include +#include namespace AZ { @@ -219,7 +220,7 @@ namespace AzToolsFramework const AZStd::vector& entities, AZStd::vector>&& instancesToConsume, AZ::IO::PathView filePath, AZStd::unique_ptr 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 diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponentInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponentInterface.h index 54ca951841..66d85ccee9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponentInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponentInterface.h @@ -78,8 +78,7 @@ namespace AzToolsFramework AZStd::unique_ptr containerEntity = nullptr, InstanceOptionalReference parent = AZStd::nullopt, bool shouldCreateLinks = true) = 0; }; - - + } // namespace Prefab } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingBus.h new file mode 100644 index 0000000000..f83bd2c6f1 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingBus.h @@ -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 +#include +#include +#include +#include +#include +#include + +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& entityIds, const AZStd::string& filePath) = 0; + }; + + using PrefabSystemScriptingBus = AZ::EBus; + + } // namespace Prefab +} // namespace AzToolsFramework + diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.cpp new file mode 100644 index 0000000000..d34e2cfc92 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.cpp @@ -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 +#include +#include +#include +#include + +namespace AzToolsFramework::Prefab +{ + void PrefabSystemScriptingHandler::Reflect(AZ::ReflectContext* context) + { + if (auto behaviorContext = azrtti_cast(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") + ->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& entityIds, const AZStd::string& filePath) + { + AZStd::vector 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(); + } +} diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.h new file mode 100644 index 0000000000..809690bf35 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemScriptingHandler.h @@ -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 + +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& entityIds, const AZStd::string& filePath) override; + ////////////////////////////////////////////////////////////////////////// + + PrefabSystemComponentInterface* m_prefabSystemComponentInterface = nullptr; + }; + } // namespace Prefab +} // namespace AzToolsFramework + diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.cpp new file mode 100644 index 0000000000..465d83e94b --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.cpp @@ -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 +#include +#include +#include +#include + +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(context); serializeContext != nullptr) + { + serializeContext->Class() + ->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(context)) + { + jsonContext->Serializer()->HandlesType(); + } + + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->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(), + "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(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() == valueTypeId, + "Unable to Serialize because the provided type is not PrefabGroup::PrefabDomData."); + + const PrefabDomData* prefabDomData = reinterpret_cast(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"); + } +} diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h new file mode 100644 index 0000000000..dd57389b7b --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h @@ -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 +#include +#include +#include + +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; + }; + +} diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/ScriptingPrefabLoader.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/ScriptingPrefabLoader.cpp new file mode 100644 index 0000000000..bee8a8bc3b --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/ScriptingPrefabLoader.cpp @@ -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 + +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 ScriptingPrefabLoader::SaveTemplateToString(TemplateId templateId) + { + AZStd::string json; + + if (m_prefabLoaderInterface->SaveTemplateToString(templateId, json)) + { + return AZ::Success(json); + } + + return AZ::Failure(); + } +} // namespace AzToolsFramework::Prefab diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/ScriptingPrefabLoader.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/ScriptingPrefabLoader.h new file mode 100644 index 0000000000..ec6219909e --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/ScriptingPrefabLoader.h @@ -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 +#include + +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 SaveTemplateToString(TemplateId templateId) override; + ////////////////////////////////////////////////////////////////////////// + + PrefabLoaderInterface* m_prefabLoaderInterface = nullptr; + }; + } // namespace Prefab +} // namespace AzToolsFramework + diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp index 07dcd2ab20..d4b227c4ff 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -25,6 +26,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -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()); + EditorRequests::Bus::Broadcast(&AzToolsFramework::EditorRequests::BrowseForAssets, selection); + + if (!selection.IsValid()) + { + return false; + } + + auto product = azrtti_cast(selection.GetResult()); + if (product == nullptr) + { + return false; + } + + outPrefabAssetPath = product->GetRelativePath(); + + auto asset = AZ::Data::AssetManager::Instance().GetAsset( + product->GetAssetId(), + azrtti_typeid(), + 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(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.h index 44e30b2013..696c05991c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.h @@ -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 diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake index 0866b225c1..ff7fe58f1e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake @@ -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 diff --git a/Code/Framework/AzToolsFramework/Tests/Entity/EntityUtilityComponentTests.cpp b/Code/Framework/AzToolsFramework/Tests/Entity/EntityUtilityComponentTests.cpp new file mode 100644 index 0000000000..fce44d4833 --- /dev/null +++ b/Code/Framework/AzToolsFramework/Tests/Entity/EntityUtilityComponentTests.cpp @@ -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 +#include + +#include +#include +#include +#include +#include + +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 g_globalComponentDetails = {}; + bool g_globalBool = false; + + class EntityUtilityComponentTests + : public ToolsApplicationFixture + { + void InitProperties() + { + AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface::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{}; + } + + 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::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::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::Get()->FindEntity(g_globalEntityId); + + ASSERT_EQ(entity, nullptr); + } + + TEST_F(EntityUtilityComponentTests, CreateEntityEmptyName) + { + AZ::ScriptContext sc; + auto behaviorContext = AZ::Interface::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::Get()->FindEntity(g_globalEntityId); + + ASSERT_NE(entity, nullptr); + } + + TEST_F(EntityUtilityComponentTests, FindComponent) + { + AZ::ScriptContext sc; + auto behaviorContext = AZ::Interface::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::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::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::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::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::Get()->FindEntity(g_globalEntityId); + + auto* transformComponent = entity->FindComponent(); + + 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::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::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::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::Get()->GetBehaviorContext(); + + sc.BindTo(behaviorContext); + sc.Execute(R"LUA( + g_globalComponentDetails = EntityUtilityBus.Broadcast.FindMatchingComponents("404") + )LUA"); + + EXPECT_EQ(g_globalComponentDetails.size(), 0); + } +} diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabScriptingTests.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabScriptingTests.cpp new file mode 100644 index 0000000000..7e61c50629 --- /dev/null +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabScriptingTests.cpp @@ -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 +#include + +#include +#include +#include + +namespace UnitTest +{ + TemplateId g_globalTemplateId = {}; + AZStd::string g_globalPrefabString = ""; + + class PrefabScriptingTest : public PrefabTestFixture + { + void InitProperties() const + { + AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface::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::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::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::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::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::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::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::Get(); + prefabSystemComponentInterface->RemoveAllTemplates(); + + EXPECT_STRNE(g_globalPrefabString.c_str(), ""); + TemplateId templateFromString = AZ::Interface::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::Get()->FindEntity(entityId); + + // Instantiate the prefab we saved + AZStd::unique_ptr instance = prefabSystemComponentInterface->InstantiatePrefab(templateFromString); + + EXPECT_NE(instance, nullptr); + + AZStd::vector 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 + } + +} diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/ProceduralPrefabAssetTests.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/ProceduralPrefabAssetTests.cpp new file mode 100644 index 0000000000..0b9e73bbac --- /dev/null +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/ProceduralPrefabAssetTests.cpp @@ -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 +#include + +#include +#include +#include +#include +#include + +namespace UnitTest +{ + class ProceduralPrefabAssetTest + : public PrefabTestFixture + { + void SetUpEditorFixtureImpl() override + { + AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface::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::Get(); + componentApplicationRequests->GetJsonRegistrationContext()->EnableRemoveReflection(); + AZ::Prefab::ProceduralPrefabAsset::Reflect(componentApplicationRequests->GetJsonRegistrationContext()); + } + }; + + TEST_F(ProceduralPrefabAssetTest, ReflectContext_AccessMethods_Works) + { + AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface::Get(); + + auto* serializeContext = componentApplicationRequests->GetSerializeContext(); + EXPECT_TRUE(!serializeContext->CreateAny(azrtti_typeid()).empty()); + EXPECT_TRUE(!serializeContext->CreateAny(azrtti_typeid()).empty()); + + auto* jsonRegistrationContext = componentApplicationRequests->GetJsonRegistrationContext(); + EXPECT_TRUE(jsonRegistrationContext->GetSerializerForSerializerType(azrtti_typeid())); + } + + 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"); + } +} diff --git a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake index 11ea5a0b38..87d522c4a6 100644 --- a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake +++ b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake @@ -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 diff --git a/Code/Tools/AssetProcessor/AssetBuilder/AssetBuilderApplication.cpp b/Code/Tools/AssetProcessor/AssetBuilder/AssetBuilderApplication.cpp index 9c9c517020..da2edcc51f 100644 --- a/Code/Tools/AssetProcessor/AssetBuilder/AssetBuilderApplication.cpp +++ b/Code/Tools/AssetProcessor/AssetBuilder/AssetBuilderApplication.cpp @@ -36,6 +36,7 @@ #include #include #include +#include namespace AssetBuilder { @@ -80,6 +81,7 @@ AZ::ComponentTypeList AssetBuilderApplication::GetRequiredSystemComponents() con azrtti_typeid(), azrtti_typeid(), azrtti_typeid(), + azrtti_typeid(), }); return components; diff --git a/Code/Tools/AssetProcessor/native/ui/AssetTreeFilterModel.cpp b/Code/Tools/AssetProcessor/native/ui/AssetTreeFilterModel.cpp index 50a472c44d..c33fc59d97 100644 --- a/Code/Tools/AssetProcessor/native/ui/AssetTreeFilterModel.cpp +++ b/Code/Tools/AssetProcessor/native/ui/AssetTreeFilterModel.cpp @@ -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); } diff --git a/Code/Tools/SceneAPI/SceneCore/Containers/GraphObjectProxy.cpp b/Code/Tools/SceneAPI/SceneCore/Containers/GraphObjectProxy.cpp index ab260eba4d..1efec493c1 100644 --- a/Code/Tools/SceneAPI/SceneCore/Containers/GraphObjectProxy.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Containers/GraphObjectProxy.cpp @@ -194,8 +194,8 @@ namespace AZ if (baseClass) { m_behaviorClass = behaviorClass; + return true; } - return true; } return false; } diff --git a/Code/Tools/SceneAPI/SceneCore/Containers/Scene.cpp b/Code/Tools/SceneAPI/SceneCore/Containers/Scene.cpp index b6c7eef90f..31b74405d2 100644 --- a/Code/Tools/SceneAPI/SceneCore/Containers/Scene.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Containers/Scene.cpp @@ -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)) diff --git a/Code/Tools/SceneAPI/SceneCore/Containers/Scene.h b/Code/Tools/SceneAPI/SceneCore/Containers/Scene.h index 49320c4592..0185c23178 100644 --- a/Code/Tools/SceneAPI/SceneCore/Containers/Scene.h +++ b/Code/Tools/SceneAPI/SceneCore/Containers/Scene.h @@ -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; diff --git a/Code/Tools/SceneAPI/SceneCore/Containers/SceneGraph.cpp b/Code/Tools/SceneAPI/SceneCore/Containers/SceneGraph.cpp index cae0dc45d5..3d00dccec3 100644 --- a/Code/Tools/SceneAPI/SceneCore/Containers/SceneGraph.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Containers/SceneGraph.cpp @@ -97,7 +97,7 @@ namespace AZ GraphObjectProxy* proxy = aznew GraphObjectProxy(graphObject); return proxy; } - return nullptr; + return aznew GraphObjectProxy(nullptr); }); } } diff --git a/Code/Tools/SceneAPI/SceneCore/Containers/SceneManifest.cpp b/Code/Tools/SceneAPI/SceneCore/Containers/SceneManifest.cpp index 664c5f1272..8e3f06b4cb 100644 --- a/Code/Tools/SceneAPI/SceneCore/Containers/SceneManifest.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Containers/SceneManifest.cpp @@ -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"; diff --git a/Code/Tools/SceneAPI/SceneCore/Containers/SceneManifest.h b/Code/Tools/SceneAPI/SceneCore/Containers/SceneManifest.h index 4386d4fd8c..f3838096ad 100644 --- a/Code/Tools/SceneAPI/SceneCore/Containers/SceneManifest.h +++ b/Code/Tools/SceneAPI/SceneCore/Containers/SceneManifest.h @@ -41,6 +41,8 @@ namespace AZ AZ_RTTI(SceneManifest, "{9274AD17-3212-4651-9F3B-7DCCB080E467}"); + static constexpr size_t MaxSceneManifestFileSizeInBytes = AZStd::numeric_limits::max(); + virtual ~SceneManifest(); static AZStd::shared_ptr SceneManifestConstDataConverter( diff --git a/Code/Tools/SceneAPI/SceneCore/Events/AssetImportRequest.cpp b/Code/Tools/SceneAPI/SceneCore/Events/AssetImportRequest.cpp index a182e4f02d..c98fa63916 100644 --- a/Code/Tools/SceneAPI/SceneCore/Events/AssetImportRequest.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Events/AssetImportRequest.cpp @@ -6,6 +6,7 @@ * */ +#include #include #include #include @@ -73,6 +74,10 @@ namespace AZ { } + void AssetImportRequest::GetGeneratedManifestExtension(AZStd::string& /*result*/) + { + } + void AssetImportRequest::GetSupportedFileExtensions(AZStd::unordered_set& /*extensions*/) { } @@ -103,14 +108,34 @@ namespace AZ AZ_UNUSED(value); } + void AssetImportRequest::GetManifestDependencyPaths(AZStd::vector&) + { + } + AZStd::shared_ptr 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 scene = AZStd::make_shared(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().c_str()); + } + // Unique pointer, will deactivate and clean up once going out of scope. SceneCore::EntityConstructor::EntityPointer loaders = SceneCore::EntityConstructor::BuildEntity("Scene Loading", loadingComponentUuid); diff --git a/Code/Tools/SceneAPI/SceneCore/Events/AssetImportRequest.h b/Code/Tools/SceneAPI/SceneCore/Events/AssetImportRequest.h index 8b6e119f99..a2446c5ff8 100644 --- a/Code/Tools/SceneAPI/SceneCore/Events/AssetImportRequest.h +++ b/Code/Tools/SceneAPI/SceneCore/Events/AssetImportRequest.h @@ -78,6 +78,8 @@ namespace AZ virtual void GetSupportedFileExtensions(AZStd::unordered_set& 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& 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). diff --git a/Code/Tools/SceneAPI/SceneCore/Import/ManifestImportRequestHandler.cpp b/Code/Tools/SceneAPI/SceneCore/Import/ManifestImportRequestHandler.cpp index 39045da5b9..f7d3f073b9 100644 --- a/Code/Tools/SceneAPI/SceneCore/Import/ManifestImportRequestHandler.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Import/ManifestImportRequestHandler.cpp @@ -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; diff --git a/Code/Tools/SceneAPI/SceneCore/Import/ManifestImportRequestHandler.h b/Code/Tools/SceneAPI/SceneCore/Import/ManifestImportRequestHandler.h index 7659f6908f..00c802cb6b 100644 --- a/Code/Tools/SceneAPI/SceneCore/Import/ManifestImportRequestHandler.h +++ b/Code/Tools/SceneAPI/SceneCore/Import/ManifestImportRequestHandler.h @@ -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; diff --git a/Code/Tools/SceneAPI/SceneData/Behaviors/ScriptProcessorRuleBehavior.cpp b/Code/Tools/SceneAPI/SceneData/Behaviors/ScriptProcessorRuleBehavior.cpp index 2e6503d195..8d413b6038 100644 --- a/Code/Tools/SceneAPI/SceneData/Behaviors/ScriptProcessorRuleBehavior.cpp +++ b/Code/Tools/SceneAPI/SceneData/Behaviors/ScriptProcessorRuleBehavior.cpp @@ -19,6 +19,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -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(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::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::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::Get()->RemoveAllTemplates(); + // attempt to load the manifest string back to a JSON-scene-manifest auto sceneManifestLoader = AZStd::make_unique(); auto loadOutcome = sceneManifestLoader->LoadFromString(manifestUpdate); @@ -347,4 +368,8 @@ namespace AZ::SceneAPI::Behaviors return Events::ProcessingResult::Ignored; } + void ScriptProcessorRuleBehavior::GetManifestDependencyPaths(AZStd::vector& paths) + { + paths.emplace_back("/scriptFilename"); + } } // namespace AZ diff --git a/Code/Tools/SceneAPI/SceneData/Behaviors/ScriptProcessorRuleBehavior.h b/Code/Tools/SceneAPI/SceneData/Behaviors/ScriptProcessorRuleBehavior.h index fa70a12b0d..a9ddf88df9 100644 --- a/Code/Tools/SceneAPI/SceneData/Behaviors/ScriptProcessorRuleBehavior.h +++ b/Code/Tools/SceneAPI/SceneData/Behaviors/ScriptProcessorRuleBehavior.h @@ -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& 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 m_exportEventHandler; diff --git a/Gems/Prefab/PrefabBuilder/CMakeLists.txt b/Gems/Prefab/PrefabBuilder/CMakeLists.txt index 450b8d0408..9aedbd3d99 100644 --- a/Gems/Prefab/PrefabBuilder/CMakeLists.txt +++ b/Gems/Prefab/PrefabBuilder/CMakeLists.txt @@ -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) diff --git a/Gems/Prefab/PrefabBuilder/PrefabBuilderModule.cpp b/Gems/Prefab/PrefabBuilder/PrefabBuilderModule.cpp index 762f459d9d..2bf7026549 100644 --- a/Gems/Prefab/PrefabBuilder/PrefabBuilderModule.cpp +++ b/Gems/Prefab/PrefabBuilder/PrefabBuilderModule.cpp @@ -8,6 +8,7 @@ #include #include +#include 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() }); } }; diff --git a/Gems/Prefab/PrefabBuilder/PrefabBuilderTests.h b/Gems/Prefab/PrefabBuilder/PrefabBuilderTests.h index 3aea173ba4..2aec317e69 100644 --- a/Gems/Prefab/PrefabBuilder/PrefabBuilderTests.h +++ b/Gems/Prefab/PrefabBuilder/PrefabBuilderTests.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace UnitTest { diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/IPrefabGroup.h b/Gems/Prefab/PrefabBuilder/PrefabGroup/IPrefabGroup.h new file mode 100644 index 0000000000..c2f8313b95 --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/IPrefabGroup.h @@ -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 +#include +#include + +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; + }; +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.cpp new file mode 100644 index 0000000000..856aba979b --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace UnitTest +{ + class PrefabBehaviorTests + : public PrefabBuilderTests + { + public: + static void SetUpTestCase() + { + // Allocator needed by SceneCore + if (!AZ::AllocatorInstance().IsReady()) + { + AZ::AllocatorInstance().Create(); + } + AZ::SceneAPI::SceneCoreStandaloneAllocator::Initialize(AZ::Environment::GetInstance()); + } + + static void TearDownTestCase() + { + AZ::SceneAPI::SceneCoreStandaloneAllocator::TearDown(); + AZ::AllocatorInstance().Destroy(); + } + + void SetUp() override + { + PrefabBuilderTests::SetUp(); + m_prefabGroupBehavior = AZStd::make_unique(); + m_prefabGroupBehavior->Activate(); + + // Mocking the asset system replacing the AssetSystem::AssetSystemComponent + AZ::Entity* systemEntity = m_app.FindEntity(AZ::SystemEntityId); + systemEntity->FindComponent()->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(); + 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(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(m_productList, m_outputDirectory, m_scene, "mock"); + } + + AZStd::unique_ptr m_preExportEventContext; + AZ::SceneAPI::Events::ExportProductList m_productList; + AZStd::string m_outputDirectory; + AZ::SceneAPI::Containers::Scene m_scene; + }; + + AZStd::unique_ptr m_prefabGroupBehavior; + testing::NiceMock 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 /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(); + 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()); + } + } +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.inl b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.inl new file mode 100644 index 0000000000..b39f782a1e --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabBehaviorTests.inl @@ -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"; + + } +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroup.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroup.cpp new file mode 100644 index 0000000000..f4dd37b86c --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroup.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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(); + 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(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + + serializeContext->Class() + ->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(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() + ->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); + } + } +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroup.h b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroup.h new file mode 100644 index 0000000000..e5c47186eb --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroup.h @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 m_prefabDomData; + }; +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp new file mode 100644 index 0000000000..13828e5f8c --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AZ::SceneAPI::Behaviors +{ + // + // ExportEventHandler + // + + struct PrefabGroupBehavior::ExportEventHandler final + : public AZ::SceneAPI::SceneCore::ExportingComponent + { + using PreExportEventContextFunction = AZStd::function; + 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([this](auto& context) + { + return this->OnPrepareForExport(context); + }); + } + + void PrefabGroupBehavior::Deactivate() + { + m_exportEventHandler.reset(); + } + + AZStd::unique_ptr PrefabGroupBehavior::CreateProductAssetData(const SceneData::PrefabGroup* prefabGroup) const + { + using namespace AzToolsFramework::Prefab; + + auto* prefabLoaderInterface = AZ::Interface::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> 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::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::Get(); + if (!instanceToTemplateInterface) + { + AZ_Error("prefab", false, "Could not get InstanceToTemplateInterface"); + return {}; + } + + // fill out a JSON DOM + auto proceduralPrefab = AZStd::make_unique(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> 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(), + {}, + AZStd::make_optional(subId)); + + return true; + } + return false; + } + + Events::ProcessingResult PrefabGroupBehavior::OnPrepareForExport(Events::PreExportEventContext& context) const + { + AZStd::vector prefabGroupCollection; + const Containers::SceneManifest& manifest = context.GetScene().GetManifest(); + + for (size_t i = 0; i < manifest.GetEntryCount(); ++i) + { + const auto* group = azrtti_cast(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(context); + if (serializeContext) + { + serializeContext->Class()->Version(1); + } + } +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.h b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.h new file mode 100644 index 0000000000..b57d30695a --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.h @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 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 m_exportEventHandler; + }; +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupTests.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupTests.cpp new file mode 100644 index 0000000000..9fb935ce9f --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupTests.cpp @@ -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 +#include +#include +#include +#include +#include + +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())); + ASSERT_NE(nullptr, serializeContext->FindClassData(azrtti_typeid())); + + 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()); + 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(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().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(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(); + 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 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}')"); + } +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/ProceduralAssetHandler.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroup/ProceduralAssetHandler.cpp new file mode 100644 index 0000000000..17ed4d9c0c --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/ProceduralAssetHandler.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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& extensions) override; + }; + + PrefabGroupAssetHandler::AssetTypeInfoHandler::AssetTypeInfoHandler() + { + AZ::AssetTypeInfoBus::Handler::BusConnect(azrtti_typeid()); + } + + PrefabGroupAssetHandler::AssetTypeInfoHandler::~AssetTypeInfoHandler() + { + AZ::AssetTypeInfoBus::Handler::BusDisconnect(azrtti_typeid()); + } + + AZ::Data::AssetType PrefabGroupAssetHandler::AssetTypeInfoHandler::GetAssetType() const + { + return azrtti_typeid(); + } + + 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& 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()); + assetCatalog->AddExtension(s_Extension.data()); + } + if (AZ::Data::AssetManager::IsReady()) + { + AZ::Data::AssetManager::Instance().RegisterHandler(this, azrtti_typeid()); + } + m_assetTypeInfoHandler = AZStd::make_shared(); + } + + 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()) + { + 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& assetTypes) + { + assetTypes.push_back(azrtti_typeid()); + } + + AZ::Data::AssetHandler::LoadResult PrefabGroupAssetHandler::LoadAssetData( + const AZ::Data::Asset& asset, + AZStd::shared_ptr stream, + [[maybe_unused]] const AZ::Data::AssetFilterCB& assetLoadFilterCB) + { + using namespace AzToolsFramework::Prefab; + + auto* proceduralPrefabAsset = asset.GetAs(); + 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::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 s_PrefabGroupAssetHandler; +} + diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/ProceduralAssetHandler.h b/Gems/Prefab/PrefabBuilder/PrefabGroup/ProceduralAssetHandler.h new file mode 100644 index 0000000000..301f8a467a --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/ProceduralAssetHandler.h @@ -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 +#include +#include +#include + +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& assetTypes) override; + AZ::Data::AssetHandler::LoadResult LoadAssetData( + const AZ::Data::Asset& asset, + AZStd::shared_ptr stream, + const AZ::Data::AssetFilterCB& assetLoadFilterCB) override; + + class AssetTypeInfoHandler; + AZStd::shared_ptr m_assetTypeInfoHandler; + }; +} diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroupTests.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroupTests.cpp new file mode 100644 index 0000000000..a63c0caa69 --- /dev/null +++ b/Gems/Prefab/PrefabBuilder/PrefabGroupTests.cpp @@ -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 +#include +#include +#include + +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())); + ASSERT_NE(nullptr, serializeContext->FindClassData(azrtti_typeid())); + + 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()); + 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(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().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(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(); + 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 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 == '{}')"); + } +} diff --git a/Gems/Prefab/PrefabBuilder/prefabbuilder_files.cmake b/Gems/Prefab/PrefabBuilder/prefabbuilder_files.cmake index 9cdf90d951..4f3bf860be 100644 --- a/Gems/Prefab/PrefabBuilder/prefabbuilder_files.cmake +++ b/Gems/Prefab/PrefabBuilder/prefabbuilder_files.cmake @@ -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 ) diff --git a/Gems/Prefab/PrefabBuilder/prefabbuilder_tests_files.cmake b/Gems/Prefab/PrefabBuilder/prefabbuilder_tests_files.cmake index 3f1ae0388b..faf2f88b5e 100644 --- a/Gems/Prefab/PrefabBuilder/prefabbuilder_tests_files.cmake +++ b/Gems/Prefab/PrefabBuilder/prefabbuilder_tests_files.cmake @@ -9,4 +9,7 @@ set(FILES PrefabBuilderTests.h PrefabBuilderTests.cpp + PrefabGroup/PrefabGroupTests.cpp + PrefabGroup/PrefabBehaviorTests.cpp + PrefabGroup/PrefabBehaviorTests.inl ) diff --git a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py index 2c63e5f90c..2578efeae2 100755 --- a/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py +++ b/Gems/PythonAssetBuilder/Editor/Scripts/scene_api/scene_data.py @@ -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) diff --git a/Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.cpp b/Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.cpp index c1a1252c6b..1d3cd60e49 100644 --- a/Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.cpp +++ b/Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,9 @@ #include #include +#include +#include +#include #include #include @@ -81,6 +85,93 @@ namespace SceneBuilder return m_cachedFingerprint.c_str(); } + void SceneBuilderWorker::PopulateSourceDependencies(const AZStd::string& manifestJson, AZStd::vector& 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 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; } diff --git a/Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.h b/Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.h index 3ecc657a3a..32456f9b2d 100644 --- a/Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.h +++ b/Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.h @@ -11,6 +11,7 @@ #include #include #include +#include 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& 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; diff --git a/Gems/SceneProcessing/Code/Tests/SceneBuilder/SceneBuilderTests.cpp b/Gems/SceneProcessing/Code/Tests/SceneBuilder/SceneBuilderTests.cpp index 5d0105f080..ba1a186e5a 100644 --- a/Gems/SceneProcessing/Code/Tests/SceneBuilder/SceneBuilderTests.cpp +++ b/Gems/SceneProcessing/Code/Tests/SceneBuilder/SceneBuilderTests.cpp @@ -11,11 +11,14 @@ #include #include #include +#include #include #include #include +#include #include #include +#include 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& 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 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::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*)); + MOCK_METHOD5(MergeSettingsFolder, bool (AZStd::string_view, const Specializations&, AZStd::string_view, AZStd::string_view, AZStd::vector*)); + 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(), _)).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); +}