diff --git a/Resources/Editor/Textures/Prefab.png b/Resources/Editor/Textures/Prefab.png new file mode 100644 index 000000000..521d21e01 Binary files /dev/null and b/Resources/Editor/Textures/Prefab.png differ diff --git a/Sources/OvCore/include/OvCore/ECS/Actor.h b/Sources/OvCore/include/OvCore/ECS/Actor.h index 80fe90319..018fb94d3 100644 --- a/Sources/OvCore/include/OvCore/ECS/Actor.h +++ b/Sources/OvCore/include/OvCore/ECS/Actor.h @@ -103,6 +103,41 @@ namespace OvCore::ECS */ uint64_t GetGUID() const; + /** + * Defines the prefab source path for this actor. + * An empty path means this actor is not linked to a prefab source. + * @param p_prefabSource + */ + void SetPrefabSource(const std::string& p_prefabSource); + + /** + * Returns the prefab source path for this actor. + */ + const std::string& GetPrefabSource() const; + + /** + * Returns true if this actor is linked to a prefab source. + */ + bool HasPrefabSource() const; + + /** + * Defines the prefab node GUID for this actor. + * This GUID identifies which prefab node this actor instance comes from. + * @param p_prefabNodeGUID + */ + void SetPrefabNodeGUID(uint64_t p_prefabNodeGUID); + + /** + * Returns the prefab node GUID for this actor. + * A value of 0 means no prefab node GUID is assigned. + */ + uint64_t GetPrefabNodeGUID() const; + + /** + * Returns true if this actor has a prefab node GUID assigned. + */ + bool HasPrefabNodeGUID() const; + /** * Set an actor as the parent of this actor * @param p_parent @@ -369,6 +404,8 @@ namespace OvCore::ECS /* Internal settings */ int64_t m_actorID; uint64_t m_guid; + std::string m_prefabSource; + uint64_t m_prefabNodeGUID = 0; bool m_destroyed = false; bool m_sleeping = true; bool m_awaked = false; diff --git a/Sources/OvCore/include/OvCore/SceneSystem/PrefabOperations.h b/Sources/OvCore/include/OvCore/SceneSystem/PrefabOperations.h new file mode 100644 index 000000000..d8b99f2bf --- /dev/null +++ b/Sources/OvCore/include/OvCore/SceneSystem/PrefabOperations.h @@ -0,0 +1,47 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +namespace OvCore::ECS +{ + class Actor; +} + +namespace OvCore::SceneSystem +{ + /** + * Utility functions to serialize and instantiate prefab files. + */ + class PrefabOperations + { + public: + /** + * Disabled constructor + */ + PrefabOperations() = delete; + + /** + * Save an actor hierarchy to a prefab file. + * @param p_rootActor + * @param p_outputPath + */ + static bool SaveToFile(OvCore::ECS::Actor& p_rootActor, const std::filesystem::path& p_outputPath); + + /** + * Instantiate a prefab file using the provided actor factory. + * @param p_prefabPath + * @param p_createActor + */ + static OvCore::ECS::Actor* InstantiateFromFile( + const std::filesystem::path& p_prefabPath, + const std::function& p_createActor + ); + }; +} diff --git a/Sources/OvCore/src/OvCore/ECS/Actor.cpp b/Sources/OvCore/src/OvCore/ECS/Actor.cpp index 0455099c7..f62aae69f 100644 --- a/Sources/OvCore/src/OvCore/ECS/Actor.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Actor.cpp @@ -140,6 +140,43 @@ uint64_t OvCore::ECS::Actor::GetGUID() const return m_guid; } +void OvCore::ECS::Actor::SetPrefabSource(const std::string& p_prefabSource) +{ + if (p_prefabSource == "?") + { + m_prefabSource.clear(); + } + else + { + m_prefabSource = p_prefabSource; + } +} + +const std::string& OvCore::ECS::Actor::GetPrefabSource() const +{ + return m_prefabSource; +} + +bool OvCore::ECS::Actor::HasPrefabSource() const +{ + return !m_prefabSource.empty(); +} + +void OvCore::ECS::Actor::SetPrefabNodeGUID(uint64_t p_prefabNodeGUID) +{ + m_prefabNodeGUID = p_prefabNodeGUID; +} + +uint64_t OvCore::ECS::Actor::GetPrefabNodeGUID() const +{ + return m_prefabNodeGUID; +} + +bool OvCore::ECS::Actor::HasPrefabNodeGUID() const +{ + return m_prefabNodeGUID != 0; +} + void OvCore::ECS::Actor::SetParent(Actor& p_parent) { DetachFromParent(); @@ -451,6 +488,8 @@ void OvCore::ECS::Actor::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XM OvCore::Helpers::Serializer::SerializeBoolean(p_doc, actorNode, "active", m_active); OvCore::Helpers::Serializer::SerializeInt64(p_doc, actorNode, "id", m_actorID); OvCore::Helpers::Serializer::SerializeUInt64(p_doc, actorNode, "guid", m_guid); + OvCore::Helpers::Serializer::SerializeString(p_doc, actorNode, "prefab_source", m_prefabSource); + OvCore::Helpers::Serializer::SerializeUInt64(p_doc, actorNode, "prefab_node_guid", m_prefabNodeGUID); OvCore::Helpers::Serializer::SerializeInt64(p_doc, actorNode, "parent", m_parentID); tinyxml2::XMLNode* componentsNode = p_doc.NewElement("components"); @@ -505,6 +544,12 @@ void OvCore::ECS::Actor::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2:: OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_actorsRoot, "active", m_active); OvCore::Helpers::Serializer::DeserializeInt64(p_doc, p_actorsRoot, "id", m_actorID); OvCore::Helpers::Serializer::DeserializeUInt64(p_doc, p_actorsRoot, "guid", m_guid); + std::string prefabSource; + OvCore::Helpers::Serializer::DeserializeString(p_doc, p_actorsRoot, "prefab_source", prefabSource); + SetPrefabSource(prefabSource); + uint64_t prefabNodeGUID = 0; + OvCore::Helpers::Serializer::DeserializeUInt64(p_doc, p_actorsRoot, "prefab_node_guid", prefabNodeGUID); + SetPrefabNodeGUID(prefabNodeGUID); OvCore::Helpers::Serializer::DeserializeInt64(p_doc, p_actorsRoot, "parent", m_parentID); { diff --git a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp index e3d47948d..cd4840c0c 100644 --- a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp +++ b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp @@ -32,6 +32,7 @@ namespace case EFileType::SOUND: return "Pick Sound"; case EFileType::SCRIPT: return "Pick Script"; case EFileType::SCENE: return "Pick Scene"; + case EFileType::PREFAB: return "Pick Prefab"; default: return "Pick Asset"; } } diff --git a/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp b/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp new file mode 100644 index 000000000..05a232d34 --- /dev/null +++ b/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp @@ -0,0 +1,132 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace +{ + void SerializeActorHierarchy( + OvCore::ECS::Actor& p_actor, + tinyxml2::XMLDocument& p_doc, + tinyxml2::XMLNode& p_actorsRoot) + { + if (!p_actor.HasPrefabNodeGUID()) + { + p_actor.SetPrefabNodeGUID(p_actor.GetGUID()); + } + + p_actor.OnSerialize(p_doc, &p_actorsRoot); + + for (auto* child : p_actor.GetChildren()) + { + SerializeActorHierarchy(*child, p_doc, p_actorsRoot); + } + } +} + +bool OvCore::SceneSystem::PrefabOperations::SaveToFile(OvCore::ECS::Actor& p_rootActor, const std::filesystem::path& p_outputPath) +{ + tinyxml2::XMLDocument doc; + + auto* rootNode = doc.NewElement("root"); + doc.InsertFirstChild(rootNode); + + auto* prefabNode = doc.NewElement("prefab"); + rootNode->InsertEndChild(prefabNode); + + auto* actorsNode = doc.NewElement("actors"); + prefabNode->InsertEndChild(actorsNode); + + SerializeActorHierarchy(p_rootActor, doc, *actorsNode); + + return doc.SaveFile(p_outputPath.string().c_str()) == tinyxml2::XML_SUCCESS; +} + +OvCore::ECS::Actor* OvCore::SceneSystem::PrefabOperations::InstantiateFromFile( + const std::filesystem::path& p_prefabPath, + const std::function& p_createActor) +{ + if (!p_createActor) + { + OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": invalid actor factory callback."); + return nullptr; + } + + tinyxml2::XMLDocument doc; + const auto loadResult = doc.LoadFile(p_prefabPath.string().c_str()); + if (loadResult != tinyxml2::XML_SUCCESS) + { + OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": XML parsing failed (code " + std::to_string(loadResult) + ")."); + return nullptr; + } + + auto* rootNode = doc.FirstChildElement("root"); + auto* prefabNode = rootNode ? rootNode->FirstChildElement("prefab") : nullptr; + auto* actorsNode = prefabNode ? prefabNode->FirstChildElement("actors") : nullptr; + + if (!actorsNode) + { + OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": missing // node."); + return nullptr; + } + + struct PendingAttachment + { + OvCore::ECS::Actor* actor = nullptr; + int64_t sourceParentID = 0; + }; + + std::vector pendingAttachments; + std::unordered_map sourceToInstance; + + for (auto* currentActor = actorsNode->FirstChildElement("actor"); + currentActor; + currentActor = currentActor->NextSiblingElement("actor")) + { + auto& newActor = p_createActor(); + const int64_t generatedID = newActor.GetID(); + const uint64_t generatedGUID = newActor.GetGUID(); + + newActor.OnDeserialize(doc, currentActor); + const uint64_t prefabNodeGUID = newActor.HasPrefabNodeGUID() ? newActor.GetPrefabNodeGUID() : newActor.GetGUID(); + newActor.SetPrefabNodeGUID(prefabNodeGUID); + + pendingAttachments.push_back({ + &newActor, + newActor.GetParentID() + }); + + sourceToInstance[newActor.GetID()] = &newActor; + + newActor.SetID(generatedID); + newActor.SetGUID(generatedGUID); + } + + OvCore::ECS::Actor* instantiatedRoot = nullptr; + + for (auto& pending : pendingAttachments) + { + if (auto found = sourceToInstance.find(pending.sourceParentID); found != sourceToInstance.end()) + { + pending.actor->SetParent(*found->second); + } + else if (!instantiatedRoot) + { + instantiatedRoot = pending.actor; + } + } + + return instantiatedRoot; +} diff --git a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h index d7f8819e3..901594d57 100644 --- a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h +++ b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h @@ -232,6 +232,34 @@ namespace OvEditor::Core bool p_focus = true, bool p_keepSourceParentIfNoForcedParent = true ); + + /** + * Save an actor hierarchy to a prefab file + * @param p_actor + * @param p_path + */ + void SaveActorAsPrefab(OvCore::ECS::Actor& p_actor, const std::string& p_path); + + /** + * Instantiate a prefab file in the current scene + * @param p_path + */ + OvCore::ECS::Actor* InstantiatePrefab(const std::string& p_path); + + /** + * Apply the current actor hierarchy state to its prefab source. + * Returns true on success. + * @param p_actor + */ + bool ApplyActorToPrefab(OvCore::ECS::Actor& p_actor); + + /** + * Revert an actor hierarchy from its prefab source. + * Keeps existing actor GUIDs to preserve scene/script references. + * Returns true on success. + * @param p_actor + */ + bool RevertActorToPrefab(OvCore::ECS::Actor& p_actor); #pragma endregion #pragma region ACTOR_MANIPULATION diff --git a/Sources/OvEditor/include/OvEditor/Panels/SceneView.h b/Sources/OvEditor/include/OvEditor/Panels/SceneView.h index edaafdf64..dcb5eb146 100644 --- a/Sources/OvEditor/include/OvEditor/Panels/SceneView.h +++ b/Sources/OvEditor/include/OvEditor/Panels/SceneView.h @@ -64,6 +64,7 @@ namespace OvEditor::Panels void OnSceneDropped(const std::string& p_path); void OnModelDropped(const std::string& p_path); void OnMaterialDropped(const std::string& p_path); + void OnPrefabDropped(const std::string& p_path); private: OvCore::SceneSystem::SceneManager& m_sceneManager; @@ -74,4 +75,4 @@ namespace OvEditor::Panels OvTools::Utils::OptRef m_highlightedActor; std::optional m_highlightedGizmoDirection; }; -} \ No newline at end of file +} diff --git a/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h b/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h index d7f2702ce..42c1d6d77 100644 --- a/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h +++ b/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h @@ -7,6 +7,7 @@ #pragma once #include +#include namespace OvUI::Widgets::Menu { diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index f6b6798c8..4b0f8eac7 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -5,6 +5,7 @@ */ #include +#include #include "OvDebug/Assertion.h" #include "OvTools/Utils/OptRef.h" #include @@ -12,6 +13,8 @@ #include #include #include +#include +#include #include #include @@ -40,6 +43,7 @@ #include #include #include +#include #include #include @@ -55,6 +59,293 @@ namespace { constexpr std::string_view kDefaultMaterialPath = ":Materials\\Default.ovmat"; + void NormalizePrefabSourcesRecursively( + OvCore::ECS::Actor& p_actor, + const std::string& p_inheritedPrefabSource) + { + if (p_actor.HasPrefabSource()) + { + const std::string currentPrefabSource = p_actor.GetPrefabSource(); + if (currentPrefabSource == p_inheritedPrefabSource) + { + p_actor.SetPrefabSource("?"); + } + } + + const std::string nextInheritedPrefabSource = + p_actor.HasPrefabSource() ? p_actor.GetPrefabSource() : p_inheritedPrefabSource; + + for (auto* child : p_actor.GetChildren()) + { + NormalizePrefabSourcesRecursively(*child, nextInheritedPrefabSource); + } + } + + void SetRootPrefabSourceAndNormalizeChildren( + OvCore::ECS::Actor& p_rootActor, + const std::string& p_newRootPrefabSource) + { + p_rootActor.SetPrefabSource(p_newRootPrefabSource); + + for (auto* child : p_rootActor.GetChildren()) + { + NormalizePrefabSourcesRecursively(*child, p_newRootPrefabSource); + } + } + + OvCore::ECS::Actor* ResolvePrefabInstanceRoot(OvCore::ECS::Actor& p_actor) + { + auto* resolvedRoot = &p_actor; + + while (resolvedRoot && !resolvedRoot->HasPrefabSource()) + { + resolvedRoot = resolvedRoot->GetParent(); + } + + if (!resolvedRoot) + { + OVLOG_ERROR("Failed to resolve prefab instance root for actor \"" + p_actor.GetName() + "\": no prefab source found in actor hierarchy."); + return nullptr; + } + + return resolvedRoot; + } + + bool TryParseUInt64(const char* p_text, uint64_t& p_value) + { + if (!p_text) + { + return false; + } + + const std::string_view text{ p_text }; + const auto [parseEnd, error] = std::from_chars(text.data(), text.data() + text.size(), p_value); + return error == std::errc{} && parseEnd == text.data() + text.size(); + } + + void RemapActorReferenceGuidsInActorNode( + tinyxml2::XMLElement& p_actorNode, + const std::unordered_map& p_guidRemap) + { + auto* behavioursNode = p_actorNode.FirstChildElement("behaviours"); + + if (!behavioursNode) + { + return; + } + + for (auto* behaviourNode = behavioursNode->FirstChildElement("behaviour"); + behaviourNode; + behaviourNode = behaviourNode->NextSiblingElement("behaviour")) + { + auto* dataNode = behaviourNode->FirstChildElement("data"); + auto* scriptPropertiesNode = dataNode ? dataNode->FirstChildElement("script_properties") : nullptr; + + if (!scriptPropertiesNode) + { + continue; + } + + for (auto* propertyNode = scriptPropertiesNode->FirstChildElement(); + propertyNode; + propertyNode = propertyNode->NextSiblingElement()) + { + const char* typeAttribute = propertyNode->Attribute("type"); + + if (!typeAttribute || std::string_view{ typeAttribute } != "actor") + { + continue; + } + + uint64_t sourceGuid = 0; + + if (!TryParseUInt64(propertyNode->GetText(), sourceGuid)) + { + continue; + } + + if (const auto it = p_guidRemap.find(sourceGuid); it != p_guidRemap.end()) + { + propertyNode->SetText(std::to_string(it->second).c_str()); + } + } + } + } + + struct ActorHierarchyEntry + { + OvCore::ECS::Actor* actor = nullptr; + OvCore::ECS::Actor* parent = nullptr; + }; + + void CollectActorHierarchy( + OvCore::ECS::Actor& p_actor, + OvCore::ECS::Actor* p_parent, + std::vector& p_outEntries) + { + p_outEntries.push_back({ &p_actor, p_parent }); + + for (auto* child : p_actor.GetChildren()) + { + CollectActorHierarchy(*child, &p_actor, p_outEntries); + } + } + + uint64_t GetPrefabNodeGUIDOrFallback(OvCore::ECS::Actor& p_actor) + { + if (p_actor.HasPrefabNodeGUID()) + { + return p_actor.GetPrefabNodeGUID(); + } + + const uint64_t fallbackGUID = p_actor.GetGUID(); + p_actor.SetPrefabNodeGUID(fallbackGUID); + return fallbackGUID; + } + + OvCore::ECS::Actor* FindUnusedActorWithPrefabNodeGUID( + const uint64_t p_prefabNodeGUID, + const std::unordered_map>& p_actorsByPrefabNodeGUID, + const std::unordered_set& p_usedActors) + { + if (const auto found = p_actorsByPrefabNodeGUID.find(p_prefabNodeGUID); found != p_actorsByPrefabNodeGUID.end()) + { + for (auto* actor : found->second) + { + if (!p_usedActors.contains(actor)) + { + return actor; + } + } + } + + return nullptr; + } + + std::unordered_map GatherSerializedGuidsByPrefabNodeGUID(const std::filesystem::path& p_prefabPath) + { + std::unordered_map serializedGuidsByPrefabNodeGUID; + + tinyxml2::XMLDocument doc; + if (doc.LoadFile(p_prefabPath.string().c_str()) != tinyxml2::XML_SUCCESS) + { + return serializedGuidsByPrefabNodeGUID; + } + + auto* rootNode = doc.FirstChildElement("root"); + auto* prefabNode = rootNode ? rootNode->FirstChildElement("prefab") : nullptr; + auto* actorsNode = prefabNode ? prefabNode->FirstChildElement("actors") : nullptr; + if (!actorsNode) + { + return serializedGuidsByPrefabNodeGUID; + } + + for (auto* actorNode = actorsNode->FirstChildElement("actor"); + actorNode; + actorNode = actorNode->NextSiblingElement("actor")) + { + const char* serializedGuidText = actorNode->FirstChildElement("guid") + ? actorNode->FirstChildElement("guid")->GetText() + : nullptr; + uint64_t serializedGUID = 0; + if (!TryParseUInt64(serializedGuidText, serializedGUID)) + { + continue; + } + + const char* prefabNodeGuidText = actorNode->FirstChildElement("prefab_node_guid") + ? actorNode->FirstChildElement("prefab_node_guid")->GetText() + : nullptr; + uint64_t prefabNodeGUID = 0; + if (!TryParseUInt64(prefabNodeGuidText, prefabNodeGUID)) + { + prefabNodeGUID = serializedGUID; + } + + serializedGuidsByPrefabNodeGUID[prefabNodeGUID] = serializedGUID; + } + + return serializedGuidsByPrefabNodeGUID; + } + + void SetActorNodeValue(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLElement& p_actorNode, const char* p_fieldName, const std::string& p_value) + { + auto* field = p_actorNode.FirstChildElement(p_fieldName); + + if (!field) + { + field = p_doc.NewElement(p_fieldName); + p_actorNode.InsertEndChild(field); + } + + field->SetText(p_value.c_str()); + } + + void RemoveActorComponentsAndBehaviours(OvCore::ECS::Actor& p_actor) + { + const auto components = p_actor.GetComponents(); + + for (const auto& component : components) + { + if (!dynamic_cast(component.get())) + { + p_actor.RemoveComponent(*component); + } + } + + const auto behaviourNames = p_actor.GetBehavioursOrder(); + + for (const auto& behaviourName : behaviourNames) + { + p_actor.RemoveBehaviour(behaviourName); + } + } + + void OverwriteActorFromPrefabTemplate( + OvCore::ECS::Actor& p_targetActor, + OvCore::ECS::Actor& p_templateActor, + const std::unordered_map& p_guidRemap, + bool p_preserveName, + bool p_preserveLocalTransform) + { + tinyxml2::XMLDocument doc; + auto* actorsNode = doc.NewElement("actors"); + doc.InsertFirstChild(actorsNode); + p_templateActor.OnSerialize(doc, actorsNode); + + auto* actorNode = actorsNode->FirstChildElement("actor"); + if (!actorNode) + { + return; + } + + RemapActorReferenceGuidsInActorNode(*actorNode, p_guidRemap); + + SetActorNodeValue(doc, *actorNode, "id", std::to_string(p_targetActor.GetID())); + SetActorNodeValue(doc, *actorNode, "guid", std::to_string(p_targetActor.GetGUID())); + SetActorNodeValue(doc, *actorNode, "parent", std::to_string(p_targetActor.GetParentID())); + + const std::string previousName = p_targetActor.GetName(); + const auto previousLocalPosition = p_targetActor.transform.GetLocalPosition(); + const auto previousLocalRotation = p_targetActor.transform.GetLocalRotation(); + const auto previousLocalScale = p_targetActor.transform.GetLocalScale(); + + RemoveActorComponentsAndBehaviours(p_targetActor); + p_targetActor.OnDeserialize(doc, actorNode); + + if (p_preserveName) + { + p_targetActor.SetName(previousName); + } + + if (p_preserveLocalTransform) + { + p_targetActor.transform.SetLocalPosition(previousLocalPosition); + p_targetActor.transform.SetLocalRotation(previousLocalRotation); + p_targetActor.transform.SetLocalScale(previousLocalScale); + } + } + void RefreshMaterialsUsingShader( OvCore::ResourceManagement::MaterialManager& p_materialManager, OvRendering::Resources::Shader& p_shader @@ -143,6 +434,7 @@ namespace } ); } + } std::string OvEditor::Core::GetBuildTypeName(OvEditor::Core::EBuildType p_buildType) @@ -888,6 +1180,194 @@ void OvEditor::Core::EditorActions::DuplicateActor DuplicateActor(*child, &newActor, false); } +void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_actor, const std::string& p_path) +{ + if (!OvCore::SceneSystem::PrefabOperations::SaveToFile(p_actor, p_path)) + { + OVLOG_ERROR("Failed to save prefab to: " + p_path); + return; + } + + const std::string prefabSourcePath = GetResourcePath(p_path); + SetRootPrefabSourceAndNormalizeChildren(p_actor, prefabSourcePath); + + OVLOG_INFO("Prefab saved to: " + p_path); +} + +OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std::string& p_path) +{ + if (!m_context.sceneManager.GetCurrentScene()) + return nullptr; + + const std::filesystem::path realPath = GetRealPath(p_path); + const std::string prefabSourcePath = GetResourcePath(realPath.string()); + + auto* instantiatedRoot = OvCore::SceneSystem::PrefabOperations::InstantiateFromFile( + realPath, + [this]() -> OvCore::ECS::Actor& + { + return CreateEmptyActor(false); + } + ); + + if (instantiatedRoot) + { + SetRootPrefabSourceAndNormalizeChildren(*instantiatedRoot, prefabSourcePath); + + const std::string prefabName = realPath.stem().string(); + if (!prefabName.empty()) + instantiatedRoot->SetName(prefabName); + } + else + { + OVLOG_ERROR("Failed to instantiate prefab from: " + realPath.string()); + } + + return instantiatedRoot; +} + +bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_actor) +{ + auto* prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); + if (!prefabInstanceRoot) + { + OVLOG_ERROR("Cannot apply actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); + return false; + } + + const std::string realPath = GetRealPath(prefabInstanceRoot->GetPrefabSource()); + + if (!OvCore::SceneSystem::PrefabOperations::SaveToFile(*prefabInstanceRoot, realPath)) + { + OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath); + return false; + } + + return true; +} + +bool OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_actor) +{ + auto* prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); + if (!prefabInstanceRoot) + { + OVLOG_ERROR("Cannot revert actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); + return false; + } + + const auto prefabSourcePath = prefabInstanceRoot->GetPrefabSource(); + const auto serializedGuidsByPrefabNodeGUID = + GatherSerializedGuidsByPrefabNodeGUID(GetRealPath(prefabSourcePath)); + + auto* prefabTemplateRoot = InstantiatePrefab(prefabSourcePath); + if (!prefabTemplateRoot) + { + return false; + } + + std::vector templateHierarchy; + CollectActorHierarchy(*prefabTemplateRoot, nullptr, templateHierarchy); + + std::vector targetHierarchy; + CollectActorHierarchy(*prefabInstanceRoot, nullptr, targetHierarchy); + + std::unordered_map> targetActorsByPrefabNodeGUID; + for (const auto& entry : targetHierarchy) + { + const uint64_t prefabNodeGUID = GetPrefabNodeGUIDOrFallback(*entry.actor); + targetActorsByPrefabNodeGUID[prefabNodeGUID].push_back(entry.actor); + } + + std::unordered_map templateToTarget; + std::unordered_set usedTargetActors; + + const uint64_t templateRootPrefabNodeGUID = GetPrefabNodeGUIDOrFallback(*prefabTemplateRoot); + prefabInstanceRoot->SetPrefabNodeGUID(templateRootPrefabNodeGUID); + templateToTarget[prefabTemplateRoot] = prefabInstanceRoot; + usedTargetActors.insert(prefabInstanceRoot); + + for (size_t index = 1; index < templateHierarchy.size(); ++index) + { + auto* templateActor = templateHierarchy[index].actor; + const uint64_t prefabNodeGUID = GetPrefabNodeGUIDOrFallback(*templateActor); + + OvCore::ECS::Actor* targetActor = FindUnusedActorWithPrefabNodeGUID( + prefabNodeGUID, + targetActorsByPrefabNodeGUID, + usedTargetActors + ); + + if (!targetActor) + { + targetActor = &CreateEmptyActor(false); + } + + targetActor->SetPrefabNodeGUID(prefabNodeGUID); + templateToTarget[templateActor] = targetActor; + usedTargetActors.insert(targetActor); + } + + std::unordered_map templateActorGuidToInstanceGuidMap; + for (const auto& [templateActor, targetActor] : templateToTarget) + { + const uint64_t prefabNodeGUID = templateActor->GetPrefabNodeGUID(); + const uint64_t targetGUID = targetActor->GetGUID(); + templateActorGuidToInstanceGuidMap[prefabNodeGUID] = targetGUID; + + if (const auto it = serializedGuidsByPrefabNodeGUID.find(prefabNodeGUID); it != serializedGuidsByPrefabNodeGUID.end()) + { + templateActorGuidToInstanceGuidMap[it->second] = targetGUID; + } + } + + for (const auto& entry : templateHierarchy) + { + auto* templateActor = entry.actor; + auto* targetActor = templateToTarget[templateActor]; + const bool isRoot = templateActor == prefabTemplateRoot; + + OverwriteActorFromPrefabTemplate( + *targetActor, + *templateActor, + templateActorGuidToInstanceGuidMap, + isRoot, /* keep root name */ + isRoot /* keep root local transform */ + ); + } + + for (size_t index = 1; index < templateHierarchy.size(); ++index) + { + auto* templateActor = templateHierarchy[index].actor; + auto* templateParent = templateHierarchy[index].parent; + auto* targetActor = templateToTarget[templateActor]; + auto* expectedParent = templateToTarget[templateParent]; + + if (targetActor->GetParent() != expectedParent) + { + targetActor->SetParent(*expectedParent); + } + } + + for (const auto& entry : targetHierarchy) + { + auto* targetActor = entry.actor; + if (targetActor == prefabInstanceRoot) + { + continue; + } + + if (!usedTargetActors.contains(targetActor)) + { + targetActor->MarkAsDestroy(); + } + } + + prefabTemplateRoot->MarkAsDestroy(); + SelectActor(*prefabInstanceRoot); + + return true; +} + void OvEditor::Core::EditorActions::CopyActor(OvCore::ECS::Actor& p_actor) { m_context.copyBuffer = Context::ActorCopyBuffer{ @@ -1380,6 +1860,7 @@ void OvEditor::Core::EditorActions::MigrateScripts() const auto newRelPath = (std::filesystem::path("Scripts") / scriptName).generic_string(); PropagateFileRenameThroughSavedFilesOfType(stem, newRelPath, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(stem, newRelPath, OvTools::Utils::PathParser::EFileType::PREFAB); } OVLOG_INFO("Scene files updated with new script paths"); @@ -1539,6 +2020,7 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa if (next != "?") { PropagateFileRenameThroughSavedFilesOfType(prev, next, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(prev, next, OvTools::Utils::PathParser::EFileType::PREFAB); } EDITOR_PANEL(Panels::Inspector, "Inspector").Refresh(); @@ -1546,9 +2028,11 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa } case OvTools::Utils::PathParser::EFileType::MATERIAL: PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); break; case OvTools::Utils::PathParser::EFileType::MODEL: PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::MATERIAL); break; case OvTools::Utils::PathParser::EFileType::SHADER: @@ -1559,6 +2043,22 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa break; case OvTools::Utils::PathParser::EFileType::SOUND: PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); + break; + case OvTools::Utils::PathParser::EFileType::PREFAB: + if (auto currentScene = m_context.sceneManager.GetCurrentScene()) + { + for (auto actor : currentScene->GetActors()) + { + if (actor->GetPrefabSource() == p_previousName) + { + actor->SetPrefabSource(p_newName); + } + } + } + + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); break; default: break; diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp index 712c65295..40e77827b 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp @@ -123,6 +123,7 @@ OvEditor::Core::EditorResources::EditorResources(const std::string& p_editorAsse {"Component", CreateTexture(texturesFolder / "Component.png")}, {"Material", CreateTexture(texturesFolder / "Material.png")}, {"Scene", CreateTexture(texturesFolder / "Scene.png")}, + {"Prefab", CreateTexture(texturesFolder / "Prefab.png")}, {"Sound", CreateTexture(texturesFolder / "Sound.png")}, {"Script", CreateTexture(texturesFolder / "Script.png")}, {"Add_Script", CreateTexture(texturesFolder / "Add_Script.png")}, diff --git a/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp b/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp index ad775f7bc..b98b11902 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -1155,6 +1156,29 @@ void OvEditor::Panels::AssetBrowser::ConsiderItem(OvUI::Widgets::Layout::TreeNod } } }; + + treeNode.AddPlugin>>("Actor").DataReceivedEvent += [this, &treeNode, path, p_isEngineItem](std::pair p_data) + { + if (!p_data.first) + { + return; + } + + const auto correctPath = m_pathUpdate.find(&treeNode) != m_pathUpdate.end() ? m_pathUpdate.at(&treeNode) : std::filesystem::path(path); + if (!ValidateFolderPath(correctPath, "Create prefab")) + { + return; + } + + const std::string actorName = p_data.first->GetName().empty() ? "Prefab" : p_data.first->GetName(); + const std::filesystem::path prefabPath = FindAvailableFilePath(correctPath / (actorName + ".ovprefab")); + + EDITOR_EXEC(SaveActorAsPrefab(*p_data.first, prefabPath.string())); + + treeNode.Open(); + treeNode.RemoveAllWidgets(); + ParseFolder(treeNode, std::filesystem::directory_entry(correctPath), p_isEngineItem); + }; } contextMenu.DestroyedEvent += [&itemGroup](const std::filesystem::path& p_deletedPath) { itemGroup.Destroy(); }; diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index 15642698d..680782ca4 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -7,6 +7,9 @@ #include "OvEditor/Panels/Hierarchy.h" #include "OvEditor/Core/EditorActions.h" +#include +#include + #include #include #include @@ -17,6 +20,7 @@ #include #include +#include #include #include @@ -32,15 +36,44 @@ #include #include +#include +#include + +#include #include "OvEditor/Core/EditorResources.h" #include "OvEditor/Utils/ActorCreationMenu.h" +namespace +{ + const OvCore::ECS::Actor* GetPrefabInstanceRoot(const OvCore::ECS::Actor& p_actor) + { + const OvCore::ECS::Actor* current = &p_actor; + + while (current) + { + if (current->HasPrefabSource()) + { + return current; + } + + current = current->GetParent(); + } + + return nullptr; + } + + bool IsPartOfPrefabInstance(const OvCore::ECS::Actor& p_actor) + { + return GetPrefabInstanceRoot(p_actor) != nullptr; + } +} + class ActorContextualMenu : public OvUI::Plugins::ContextualMenu { public: ActorContextualMenu(OvCore::ECS::Actor* p_target, OvUI::Widgets::Layout::TreeNode* p_treeNode = nullptr, bool p_panelMenu = false) : - m_target(p_target), + m_targetActor(p_target), m_treeNode(p_treeNode) { using namespace OvUI::Panels; @@ -48,36 +81,110 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu using namespace OvUI::Widgets::Menu; using namespace OvCore::ECS::Components; - if (m_target) + if (m_targetActor) { auto& focusButton = CreateWidget("Focus"); focusButton.ClickedEvent += [this] { - EDITOR_EXEC(MoveToTarget(*m_target)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(MoveToTarget(*target)); + } }; auto& copyButton = CreateWidget("Copy"); copyButton.ClickedEvent += [this] { - EDITOR_EXEC(CopyActor(*m_target)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(CopyActor(*target)); + } }; auto& duplicateButton = CreateWidget("Duplicate"); duplicateButton.ClickedEvent += [this] { - EDITOR_EXEC(DelayAction(EDITOR_BIND(DuplicateActor, std::ref(*m_target), nullptr, true, true), 0)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DelayAction(EDITOR_BIND(DuplicateActor, std::ref(*target), nullptr, true, true), 0)); + } }; auto& pasteButton = CreateWidget("Paste"); pasteButton.ClickedEvent += [this] { - EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, m_target), 0)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, target), 0)); + } + }; + + auto& saveAsPrefabButton = CreateWidget("Save as Prefab..."); + saveAsPrefabButton.ClickedEvent += [this] + { + auto* target = GetTargetActor(); + if (!target) + { + return; + } + + OvWindowing::Dialogs::SaveFileDialog dialog("Save Prefab"); + dialog.SetInitialDirectory(EDITOR_CONTEXT(projectAssetsPath).string()); + dialog.SetInitialFilename(target->GetName()); + dialog.DefineExtension("Overload Prefab", ".ovprefab"); + dialog.Show(); + + if (!dialog.HasSucceeded()) + { + return; + } + + if (dialog.IsFileExisting()) + { + OvWindowing::Dialogs::MessageBox message( + "File already exists!", + "The file \"" + dialog.GetSelectedFileName() + "\" already exists.\n\nOverwriting this file will replace the previous prefab content.\n\nAre you ok with that?", + OvWindowing::Dialogs::MessageBox::EMessageType::WARNING, + OvWindowing::Dialogs::MessageBox::EButtonLayout::YES_NO, + true + ); + + if (message.GetUserAction() != OvWindowing::Dialogs::MessageBox::EUserAction::YES) + { + return; + } + } + + EDITOR_EXEC(SaveActorAsPrefab(*target, dialog.GetSelectedFilePath())); + }; + + auto& applyToPrefabButton = CreateWidget("Apply to Prefab"); + m_applyToPrefabButton = &applyToPrefabButton; + applyToPrefabButton.ClickedEvent += [this] + { + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(ApplyActorToPrefab(*target)); + } + }; + + auto& revertToPrefabButton = CreateWidget("Revert to Prefab"); + m_revertToPrefabButton = &revertToPrefabButton; + revertToPrefabButton.ClickedEvent += [this] + { + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(RevertActorToPrefab(*target)); + } }; auto& deleteButton = CreateWidget("Delete"); deleteButton.ClickedEvent += [this] { - EDITOR_EXEC(DestroyActor(std::ref(*m_target))); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DestroyActor(std::ref(*target))); + } }; auto& renameMenu = CreateWidget("Rename to..."); @@ -87,12 +194,18 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu renameMenu.ClickedEvent += [this, &nameEditor] { - nameEditor.content = m_target->GetName(); + if (auto* target = GetTargetActor()) + { + nameEditor.content = target->GetName(); + } }; nameEditor.EnterPressedEvent += [this](std::string p_newName) { - m_target->SetName(p_newName); + if (auto* target = GetTargetActor()) + { + target->SetName(p_newName); + } }; } else @@ -113,18 +226,60 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu ) : std::nullopt; - OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(createActor, m_target, onItemClicked); + OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu( + createActor, + GetTargetActor(), + onItemClicked + ); } virtual void Execute(OvUI::Plugins::EPluginExecutionContext p_context) override { + if (m_applyToPrefabButton || m_revertToPrefabButton) + { + auto* target = GetTargetActor(); + const bool canEditPrefab = target && GetPrefabInstanceRoot(*target) != nullptr; + + if (m_applyToPrefabButton) + { + m_applyToPrefabButton->enabled = canEditPrefab; + } + + if (m_revertToPrefabButton) + { + m_revertToPrefabButton->enabled = canEditPrefab; + } + } + if (m_widgets.size() > 0) OvUI::Plugins::ContextualMenu::Execute(p_context); } private: - OvCore::ECS::Actor* m_target; + OvCore::ECS::Actor* GetTargetActor() const + { + if (!m_targetActor) + { + return nullptr; + } + + auto* currentScene = EDITOR_CONTEXT(sceneManager).GetCurrentScene(); + if (!currentScene) + { + return nullptr; + } + + const auto& actors = currentScene->GetActors(); + const bool actorIsStillInScene = + std::find(actors.begin(), actors.end(), m_targetActor) != actors.end(); + + return actorIsStillInScene ? m_targetActor : nullptr; + } + + OvCore::ECS::Actor* m_targetActor = nullptr; OvUI::Widgets::Layout::TreeNode* m_treeNode; + OvUI::Widgets::Menu::MenuItem* m_applyToPrefabButton = nullptr; + OvUI::Widgets::Menu::MenuItem* m_revertToPrefabButton = nullptr; }; void ExpandTreeNode(OvUI::Widgets::Layout::TreeNode& p_toExpand) @@ -364,8 +519,10 @@ void OvEditor::Panels::Hierarchy::AddActorByInstance(OvCore::ECS::Actor & p_acto auto& textSelectable = m_actors.CreateWidget(p_actor.GetName(), true); textSelectable.leaf = true; - if (auto* actorTexture = EDITOR_CONTEXT(editorResources)->GetTexture("Actor")) - textSelectable.iconTextureID = actorTexture->GetTexture().GetID(); + const uint32_t actorIconID = OvCore::Helpers::GUIHelpers::GetActorIconID(); + const uint32_t prefabIconID = OvCore::Helpers::GUIHelpers::GetIconForFileType(OvTools::Utils::PathParser::EFileType::PREFAB); + + textSelectable.iconTextureID = IsPartOfPrefabInstance(p_actor) && prefabIconID != 0 ? prefabIconID : actorIconID; textSelectable.AddPlugin(&p_actor, &textSelectable); textSelectable.AddPlugin>>("Actor", "Attach to...", std::make_pair(&p_actor, &textSelectable)); @@ -387,13 +544,15 @@ void OvEditor::Panels::Hierarchy::AddActorByInstance(OvCore::ECS::Actor & p_acto auto& dispatcher = textSelectable.AddPlugin>(); OvCore::ECS::Actor* targetPtr = &p_actor; - dispatcher.RegisterGatherer([targetPtr, &textSelectable] + dispatcher.RegisterGatherer([targetPtr, &textSelectable, actorIconID, prefabIconID] { const bool isActive = targetPtr->IsActive(); textSelectable.labelColor = isActive ? OVUI_STYLE(Text) : OVUI_STYLE(TextDisabled); + textSelectable.iconTextureID = IsPartOfPrefabInstance(*targetPtr) && prefabIconID != 0 ? prefabIconID : actorIconID; + return targetPtr->GetName(); }); diff --git a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp index 1619ee72f..09273b619 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp @@ -216,7 +216,14 @@ void OvEditor::Panels::Inspector::_PopulateActorInfo() [this] { return std::format("{:016X}", m_targetActor->GetGUID()); } ); - _DrawAddSection(); + auto& prefabSourceField = OvCore::Helpers::GUIDrawer::DrawAsset( + headerColumns, + "Prefab Source", + [this] { return m_targetActor->GetPrefabSource(); }, + [](const std::string&) {}, + OvTools::Utils::PathParser::EFileType::PREFAB + ); + prefabSourceField.disabled = true; } void OvEditor::Panels::Inspector::_PopulateActorComponents() diff --git a/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp b/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp index a030d2244..3cb964f59 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp @@ -61,6 +61,7 @@ OvEditor::Panels::SceneView::SceneView case SCENE: OnSceneDropped(path); break; case MODEL: OnModelDropped(path); break; case MATERIAL: OnMaterialDropped(path); break; + case PREFAB: OnPrefabDropped(path); break; default: break; } }; @@ -300,3 +301,11 @@ void OvEditor::Panels::SceneView::OnMaterialDropped(const std::string& p_path) } } } + +void OvEditor::Panels::SceneView::OnPrefabDropped(const std::string& p_path) +{ + if (auto* actor = EDITOR_EXEC(InstantiatePrefab(p_path)); actor) + { + EDITOR_EXEC(SelectActor(*actor)); + } +} diff --git a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp index 1a695df58..6e8ae56e5 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp @@ -19,27 +19,34 @@ #include #include #include +#include #include #include +#include + #include #include namespace { - std::function Combine(std::function p_a, std::optional> p_b) + OvCore::ECS::Actor* ResolveAliveParent(OvCore::ECS::Actor* p_parent) { - if (p_b.has_value()) + if (!p_parent) { - return [=]() - { - p_a(); - p_b.value()(); - }; + return nullptr; } - return p_a; + auto* currentScene = EDITOR_CONTEXT(sceneManager).GetCurrentScene(); + if (!currentScene) + { + return nullptr; + } + + const auto& actors = currentScene->GetActors(); + const bool parentIsStillInScene = std::find(actors.begin(), actors.end(), p_parent) != actors.end(); + return parentIsStillInScene ? p_parent : nullptr; } void CreateSkysphere(OvCore::ECS::Actor* p_parent) @@ -122,27 +129,101 @@ namespace template std::function ActorWithComponentCreationHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return Combine(EDITOR_BIND(CreateMonoComponentActor, true, p_parent), p_onItemClicked); + return [p_parent, p_onItemClicked]() + { + EDITOR_EXEC(CreateMonoComponentActor(true, ResolveAliveParent(p_parent))); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function ActorWithModelComponentCreationHandler(OvCore::ECS::Actor* p_parent, const std::string& p_modelName, std::optional> p_onItemClicked) { - return Combine(EDITOR_BIND(CreateActorWithModel, ":Models\\" + p_modelName + ".fbx", true, p_parent, p_modelName), p_onItemClicked); + return [p_parent, p_modelName, p_onItemClicked]() + { + EDITOR_EXEC(CreateActorWithModel(":Models\\" + p_modelName + ".fbx", true, ResolveAliveParent(p_parent), p_modelName)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function CreateSkysphereHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateSkysphere, p_parent), p_onItemClicked); + return [p_parent, p_onItemClicked]() + { + CreateSkysphere(ResolveAliveParent(p_parent)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function CreateAtmosphereHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateAtmosphere, p_parent), p_onItemClicked); + return [p_parent, p_onItemClicked]() + { + CreateAtmosphere(ResolveAliveParent(p_parent)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function CreateCharacterHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateCharacter, p_parent), p_onItemClicked); + return [p_parent, p_onItemClicked]() + { + CreateCharacter(ResolveAliveParent(p_parent)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; + } + + std::function CreateFromPrefabHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + { + return [p_parent, p_onItemClicked]() + { + OvCore::Helpers::GUIHelpers::OpenAssetPicker( + OvTools::Utils::PathParser::EFileType::PREFAB, + [p_parent, p_onItemClicked](std::string p_prefabPath) + { + if (p_prefabPath.empty()) + { + return; + } + + if (auto* actor = EDITOR_EXEC(InstantiatePrefab(p_prefabPath)); actor) + { + if (auto* parent = ResolveAliveParent(p_parent); parent) + { + actor->SetParent(*parent); + } + + EDITOR_EXEC(SelectActor(*actor)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + } + }, + true, + false + ); + }; } } @@ -151,7 +232,16 @@ void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets using namespace OvUI::Widgets::Menu; using namespace OvCore::ECS::Components; - p_menuList.CreateWidget("Create Empty").ClickedEvent += Combine(EDITOR_BIND(CreateEmptyActor, true, p_parent, ""), p_onItemClicked); + p_menuList.CreateWidget("Create Empty").ClickedEvent += [p_parent, p_onItemClicked]() + { + EDITOR_EXEC(CreateEmptyActor(true, ResolveAliveParent(p_parent), "")); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; + p_menuList.CreateWidget("From prefab...").ClickedEvent += CreateFromPrefabHandler(p_parent, p_onItemClicked); auto& primitives = p_menuList.CreateWidget("Primitives"); auto& physicals = p_menuList.CreateWidget("Physicals"); diff --git a/Sources/OvTools/include/OvTools/Utils/PathParser.h b/Sources/OvTools/include/OvTools/Utils/PathParser.h index 7b5af46bc..e41450a10 100644 --- a/Sources/OvTools/include/OvTools/Utils/PathParser.h +++ b/Sources/OvTools/include/OvTools/Utils/PathParser.h @@ -28,6 +28,7 @@ namespace OvTools::Utils MATERIAL, SOUND, SCENE, + PREFAB, SCRIPT, FONT }; diff --git a/Sources/OvTools/src/OvTools/Utils/PathParser.cpp b/Sources/OvTools/src/OvTools/Utils/PathParser.cpp index 074538861..3195eb67a 100644 --- a/Sources/OvTools/src/OvTools/Utils/PathParser.cpp +++ b/Sources/OvTools/src/OvTools/Utils/PathParser.cpp @@ -102,6 +102,7 @@ std::string OvTools::Utils::PathParser::FileTypeToString(EFileType p_fileType) case OvTools::Utils::PathParser::EFileType::MATERIAL: return "Material"; case OvTools::Utils::PathParser::EFileType::SOUND: return "Sound"; case OvTools::Utils::PathParser::EFileType::SCENE: return "Scene"; + case OvTools::Utils::PathParser::EFileType::PREFAB: return "Prefab"; case OvTools::Utils::PathParser::EFileType::SCRIPT: return "Script"; case OvTools::Utils::PathParser::EFileType::FONT: return "Font"; default: return "Unknown"; @@ -115,6 +116,7 @@ OvTools::Utils::PathParser::EFileType OvTools::Utils::PathParser::StringToFileTy if (p_type == "Shader") return EFileType::SHADER; if (p_type == "Material") return EFileType::MATERIAL; if (p_type == "Sound") return EFileType::SOUND; + if (p_type == "Prefab") return EFileType::PREFAB; return EFileType::UNKNOWN; } @@ -130,6 +132,7 @@ OvTools::Utils::PathParser::EFileType OvTools::Utils::PathParser::GetFileType(co else if (ext == "ovmat") return EFileType::MATERIAL; else if (ext == "wav" || ext == "mp3" || ext == "ogg") return EFileType::SOUND; else if (ext == "ovscene") return EFileType::SCENE; + else if (ext == "ovprefab") return EFileType::PREFAB; else if (ext == "lua" || ext == "ovscript") return EFileType::SCRIPT; else if (ext == "ttf") return EFileType::FONT; diff --git a/premake5.lua b/premake5.lua index db46e9c0d..99ea0ee9f 100644 --- a/premake5.lua +++ b/premake5.lua @@ -34,6 +34,7 @@ workspace "Overload" -- Set toolset based on operating system filter {"system:windows"} toolset("msc") + buildoptions { "/FS" } -- Prevent C1041 PDB write conflicts with parallel compilation filter {"system:linux"} toolset("clang") -- Use Clang on Linux (sol2 doesn't work well with GCC) filter {}