Files
o3de/Gems/Maestro/Code/Source/Components/EditorSequenceComponent.cpp
T
Mike Balfour d34d088191 Miscellaneous prefab/converter bugfixes to support TrackView (#1701)
This has a small bundle of bugfixes and improvements all based around improving prefab TrackView support:

* JsonMerger - improved the error message when patch remove operations fail to make the specific failure more obvious
Instance - swapped the order of destroying entities vs clearing the lookup tables so that lookups still produce valid results during destruction. (This could happen while creating undo caches)
* InstanceEntityIdMapper - in the case where an id isn't found, it now returns an invalid id instead of an "attempted-valid" one that still generally turned out to be not-valid
* PrefabUndo - downgraded a potential crash to an error message if for some reason the patch contains changes to an entity that doesn't currently have an alias. (This case can be caused occasionally by other bugs and error conditions)
* EditorSequenceComponent - downgraded a potential crash to an assert for the times when it tries to remove components, fails, but thinks it succeeded. (This case can currently be caused by using Maestro with Prefabs enabled)
* EditorSequenceAgentComponent - added an undo cache refresh whenever the component deletes itself, so that deleting itself during an EditorSequenceComponent destruction chain of events leaves the undo cache in the correct state.
* SliceConverter - fixed the conversion of entity references in top-level slice instance entities that refer down to nested slice entities. There was a chicken-and-egg problem in terms of which entities need to be created first to make the references and the prefab patching & serialization happy. This was worked around by creating placeholder top-level entities, then the nested slice entities, then replacing the top-level entities with the fully-realized ones.

Specific changes:
* Added more informative error message.

Signed-off-by: mbalfour <mbalfour@amazon.com>
(cherry picked from commit 672608a6c833c07295996cd9b3449825222b74d0)

* Changed the error condition to produce a "valid" invalid id instead of a deterministic but not-valid id

Signed-off-by: mbalfour <mbalfour@amazon.com>
(cherry picked from commit 3673950c949de8e067b32ddafaffd07e648a13d8)

* Guard against invalid reference assert/crash

Signed-off-by: mbalfour <mbalfour@amazon.com>
(cherry picked from commit 268d4ef3447f268a1372d07e028b9e67bac5c64e)

* Downgrade an invalid reference crash to an assert

Signed-off-by: mbalfour <mbalfour@amazon.com>
(cherry picked from commit 38c9303770845f4e863273dd6fb8fc7e83380425)

* Improved logic for handling entity references across nested slices.

Signed-off-by: mbalfour <mbalfour@amazon.com>
(cherry picked from commit 7e89a016d95fb72cb5f119e1e3768daa60e6bfb4)

* Changed order of entities.clear() call so that instance lookups are still valid during entity destruction.

Signed-off-by: mbalfour <mbalfour@amazon.com>

* Add undo cache notification when removing Maestro components.

Signed-off-by: mbalfour <mbalfour@amazon.com>
2021-07-01 13:53:17 -05:00

454 lines
22 KiB
C++

/*
* 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 "Maestro_precompiled.h"
#include "EditorSequenceComponent.h"
#include "EditorSequenceAgentComponent.h"
#include "Objects/EntityObject.h"
#include "TrackView/TrackViewSequenceManager.h"
#include <Maestro/Types/AnimValueType.h>
#include <Maestro/Types/SequenceType.h>
#include <Maestro/Types/AnimNodeType.h>
#include <AzCore/Math/Uuid.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzToolsFramework/API/ComponentEntityObjectBus.h>
#include <Maestro/Bus/SequenceAgentComponentBus.h>
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
namespace Maestro
{
/*static*/ AZ::ScriptTimePoint EditorSequenceComponent::s_lastPropertyRefreshTime;
/*static*/ const double EditorSequenceComponent::s_refreshPeriodMilliseconds = 200.0; // 5 Hz refresh rate
/*static*/ const int EditorSequenceComponent::s_invalidSequenceId = -1;
namespace ClassConverters
{
static bool UpVersionAnimationData(AZ::SerializeContext&, AZ::SerializeContext::DataElementNode&);
} // namespace ClassConverters
///////////////////////////////////////////////////////////////////////////////////////////////////////
EditorSequenceComponent::EditorSequenceComponent()
: m_sequenceId(s_invalidSequenceId)
{
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
EditorSequenceComponent::~EditorSequenceComponent()
{
bool isDuringUndo = false;
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(isDuringUndo, &AzToolsFramework::ToolsApplicationRequests::Bus::Events::IsDuringUndoRedo);
// Don't RemoveEntityToAnimate if we are in the middle of an Undo event.
// Doing so will create will mark this entity dirty and break the undo system.
if (!isDuringUndo && m_sequence)
{
for (int i = m_sequence->GetNodeCount(); --i >= 0;)
{
IAnimNode* animNode = m_sequence->GetNode(i);
if (animNode->GetType() == AnimNodeType::AzEntity)
{
RemoveEntityToAnimate(animNode->GetAzEntityId());
}
}
}
IEditor* editor = nullptr;
EBUS_EVENT_RESULT(editor, AzToolsFramework::EditorRequests::Bus, GetEditor);
if (editor)
{
IAnimSequence* sequence = editor->GetMovieSystem()->FindSequenceById(m_sequenceId);
ITrackViewSequenceManager* pSequenceManager = editor->GetSequenceManager();
if (sequence && pSequenceManager && pSequenceManager->GetSequenceByEntityId(sequence->GetSequenceEntityId()))
{
pSequenceManager->OnDeleteSequenceEntity(sequence->GetSequenceEntityId());
}
}
if (m_sequence)
{
m_sequence = nullptr;
m_sequenceId = s_invalidSequenceId;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
/*static*/ void EditorSequenceComponent::Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<AnimSerialize::AnimationData>()
->Field("SerializedString", &AnimSerialize::AnimationData::m_serializedData)
->Version(1, &ClassConverters::UpVersionAnimationData);
serializeContext->Class<EditorSequenceComponent, EditorComponentBase>()
->Field("Sequence", &EditorSequenceComponent::m_sequence)
->Version(4);
AZ::EditContext* editContext = serializeContext->GetEditContext();
if (editContext)
{
editContext->Class<EditorSequenceComponent>(
"Sequence", "Plays Cinematic Animations")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "Cinematics")
->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/Sequence.png")
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/Sequence.png")
//->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"))
->Attribute(AZ::Edit::Attributes::AddableByUser, false) // SequenceAgents are only added by TrackView
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
;
}
}
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
if (behaviorContext)
{
behaviorContext->Class<EditorSequenceComponent>()->RequestBus("SequenceComponentRequestBus");
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::Init()
{
EditorComponentBase::Init();
m_sequenceId = s_invalidSequenceId;
IEditor* editor = nullptr;
EBUS_EVENT_RESULT(editor, AzToolsFramework::EditorRequests::Bus, GetEditor);
if (editor)
{
bool sequenceWasDeserialized = false;
if (m_sequence)
{
// m_sequence is already filled if the component it was deserialized - register it with Track View
sequenceWasDeserialized = true;
editor->GetSequenceManager()->OnCreateSequenceComponent(m_sequence);
}
else
{
// if m_sequence is NULL, we're creating a new sequence - request the creation from the Track view
m_sequence = static_cast<CAnimSequence*>(editor->GetSequenceManager()->OnCreateSequenceObject(m_entity->GetName().c_str(), false, GetEntityId()));
}
if (m_sequence)
{
m_sequenceId = m_sequence->GetId();
}
if (sequenceWasDeserialized)
{
// Notify Trackview of the load
ITrackViewSequence* trackViewSequence = editor->GetSequenceManager()->GetSequenceByEntityId(GetEntityId());
if (trackViewSequence)
{
trackViewSequence->Load();
}
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::Activate()
{
EditorComponentBase::Activate();
Maestro::EditorSequenceComponentRequestBus::Handler::BusConnect(GetEntityId());
Maestro::SequenceComponentRequestBus::Handler::BusConnect(GetEntityId());
IEditor* editor = nullptr;
EBUS_EVENT_RESULT(editor, AzToolsFramework::EditorRequests::Bus, GetEditor);
if (editor)
{
editor->GetSequenceManager()->OnSequenceActivated(GetEntityId());
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::Deactivate()
{
Maestro::EditorSequenceComponentRequestBus::Handler::BusDisconnect();
Maestro::SequenceComponentRequestBus::Handler::BusDisconnect();
// disconnect from TickBus if we're connected (which would only happen if we deactivated during a pending property refresh)
AZ::TickBus::Handler::BusDisconnect();
EditorComponentBase::Deactivate();
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::AddEntityToAnimate(AZ::EntityId entityToAnimate)
{
Maestro::EditorSequenceAgentComponent* agentComponent = nullptr;
auto component = AzToolsFramework::FindComponent<EditorSequenceAgentComponent>::OnEntity(entityToAnimate);
if (component)
{
agentComponent = static_cast<Maestro::EditorSequenceAgentComponent*>(component);
}
else
{
// #TODO LY-21846: Use "SequenceAgentComponentService" to find component, rather than specific component-type.
auto addComponentResult = AzToolsFramework::AddComponents<EditorSequenceAgentComponent>::ToEntities(entityToAnimate);
if (addComponentResult.IsSuccess())
{
if (!addComponentResult.GetValue()[entityToAnimate].m_componentsAdded.empty())
{
// We need to register our Entity and Component Ids with the SequenceAgentComponent so we can communicate over EBuses
// with it. We can't do this registration over an EBus because we haven't registered with it yet - do it with pointers?
// Is this safe?
agentComponent = static_cast<Maestro::EditorSequenceAgentComponent*>(
addComponentResult.GetValue()[entityToAnimate].m_componentsAdded[0]);
}
else
{
AZ_Assert(
!addComponentResult.GetValue()[entityToAnimate].m_componentsAdded.empty(),
"Add component result was successful, but the EditorSequenceAgentComponent wasn't added. "
"This can happen if the entity id isn't found for some reason: entity id = %s",
entityToAnimate.ToString().c_str());
}
}
}
AZ_Assert(agentComponent, "EditorSequenceComponent::AddEntityToAnimate unable to create or find sequenceAgentComponent.");
// Notify the SequenceAgentComponent that we're connected to it - after this call, all communication with the Agent is over an EBus
if (agentComponent)
{
agentComponent->ConnectSequence(GetEntityId());
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::RemoveEntityToAnimate(AZ::EntityId removedEntityId)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), removedEntityId);
// Notify the SequenceAgentComponent that we're disconnecting from it
EBUS_EVENT_ID(ebusId, Maestro::SequenceAgentComponentRequestBus, DisconnectSequence);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::GetAllAnimatablePropertiesForComponent(IAnimNode::AnimParamInfos& properties, AZ::EntityId animatedEntityId, AZ::ComponentId componentId)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
Maestro::EditorSequenceAgentComponentRequestBus::Event(ebusId, &Maestro::EditorSequenceAgentComponentRequestBus::Events::GetAllAnimatableProperties, properties, componentId);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::GetAnimatableComponents(AZStd::vector<AZ::ComponentId>& componentIds, AZ::EntityId animatedEntityId)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
EBUS_EVENT_ID(ebusId, Maestro::EditorSequenceAgentComponentRequestBus, GetAnimatableComponents, componentIds);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::Uuid EditorSequenceComponent::GetAnimatedAddressTypeId(const AZ::EntityId& animatedEntityId, const Maestro::SequenceComponentRequests::AnimatablePropertyAddress& animatableAddress)
{
AZ::Uuid typeId = AZ::Uuid::CreateNull();
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
Maestro::SequenceAgentComponentRequestBus::EventResult(typeId, ebusId, &Maestro::SequenceAgentComponentRequestBus::Events::GetAnimatedAddressTypeId, animatableAddress);
return typeId;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::GetAssetDuration(AnimatedValue& returnValue, const AZ::EntityId& animatedEntityId, AZ::ComponentId componentId, const AZ::Data::AssetId& assetId)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
EBUS_EVENT_ID(ebusId, Maestro::SequenceAgentComponentRequestBus, GetAssetDuration, returnValue, componentId, assetId);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::BuildGameEntity(AZ::Entity* gameEntity)
{
SequenceComponent *gameSequenceComponent = gameEntity->CreateComponent<SequenceComponent>();
gameSequenceComponent->m_sequence = m_sequence;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
AnimValueType EditorSequenceComponent::GetValueType([[maybe_unused]] const AZStd::string& animatableAddress)
{
// TODO: look up type from BehaviorContext Property
return AnimValueType::Float;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
bool EditorSequenceComponent::SetAnimatedPropertyValue(const AZ::EntityId& animatedEntityId, const Maestro::SequenceComponentRequests::AnimatablePropertyAddress& animatableAddress, const AnimatedValue& value)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
bool changed = false;
bool animatedEntityIsSelected = false;
// put this component on the TickBus to refresh propertyGrids if it is Selected (and hence it's values will be shown in the EntityInspector)
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(animatedEntityIsSelected, &AzToolsFramework::ToolsApplicationRequests::Bus::Events::IsSelected, animatedEntityId);
if (animatedEntityIsSelected && !AZ::TickBus::Handler::BusIsConnected())
{
AZ::TickBus::Handler::BusConnect();
}
EBUS_EVENT_ID_RESULT(changed, ebusId, Maestro::SequenceAgentComponentRequestBus, SetAnimatedPropertyValue, animatableAddress, value);
return changed;
}
void EditorSequenceComponent::OnTick([[maybe_unused]] float deltaTime, AZ::ScriptTimePoint time)
{
// refresh the property displays at a lower refresh rate
if ((time.GetMilliseconds() - s_lastPropertyRefreshTime.GetMilliseconds()) > s_refreshPeriodMilliseconds)
{
s_lastPropertyRefreshTime = time;
// refresh
AzToolsFramework::ToolsApplicationEvents::Bus::Broadcast(&AzToolsFramework::ToolsApplicationEvents::Bus::Events::InvalidatePropertyDisplay, AzToolsFramework::Refresh_Values);
// disconnect from tick bus now that we've refreshed
AZ::TickBus::Handler::BusDisconnect();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::GetAnimatedPropertyValue(AnimatedValue& returnValue, const AZ::EntityId& animatedEntityId, const Maestro::SequenceComponentRequests::AnimatablePropertyAddress& animatableAddress)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
EBUS_EVENT_ID(ebusId, Maestro::SequenceAgentComponentRequestBus, GetAnimatedPropertyValue, returnValue, animatableAddress);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
bool EditorSequenceComponent::MarkEntityAsDirty() const
{
bool retSuccess = false;
AZ::Entity* entity = nullptr;
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, GetEntityId());
if (entity)
{
CEntityObject* entityObject = nullptr;
EBUS_EVENT_ID_RESULT(entityObject, GetEntityId(), AzToolsFramework::ComponentEntityEditorRequestBus, GetSandboxObject);
if (entityObject)
{
entityObject->SetModified(false);
retSuccess = true;
}
}
return retSuccess;
}
//=========================================================================
namespace ClassConverters
{
// recursively traverses XML tree rooted at node converting transform nodes. Returns true if any node was converted.
static bool convertTransformXMLNodes(XmlNodeRef node)
{
bool nodeConverted = false;
// recurse through children
for (int i = node->getChildCount(); --i >= 0;)
{
if (convertTransformXMLNodes(node->getChild(i)))
{
nodeConverted = true;
}
}
XmlString nodeType;
if (node->isTag("Node") && node->getAttr("Type", nodeType) && nodeType == "Component")
{
XmlString componentTypeId;
if (node->getAttr("ComponentTypeId", componentTypeId) && componentTypeId == "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0}") // Type Uuid AZ::EditorTransformComponentTypeId
{
static const char* paramTypeName = "paramType";
static const char* paramUserValueName = "paramUserValue";
static const char* virtualPropertyName = "virtualPropertyName";
// go through child nodes. Convert previous Position, Rotation or Scale tracks ByString to enumerated param types
for (const XmlNodeRef& childNode : node)
{
XmlString paramType;
if (childNode->isTag("Track") && childNode->getAttr(paramTypeName, paramType) && paramType == "ByString")
{
XmlString paramUserValue;
if (childNode->getAttr(paramUserValueName, paramUserValue) && paramUserValue == "Position")
{
childNode->setAttr(paramTypeName, "Position");
childNode->setAttr(virtualPropertyName, "Position");
childNode->delAttr(paramUserValueName);
nodeConverted = true;
}
else if (childNode->getAttr(paramUserValueName, paramUserValue) && paramUserValue == "Rotation")
{
childNode->setAttr(paramTypeName, "Rotation");
childNode->setAttr(virtualPropertyName, "Rotation");
childNode->delAttr(paramUserValueName);
nodeConverted = true;
}
else if (childNode->getAttr(paramUserValueName, paramUserValue) && paramUserValue == "Scale")
{
childNode->setAttr(paramTypeName, "Scale");
childNode->setAttr(virtualPropertyName, "Scale");
childNode->delAttr(paramUserValueName);
nodeConverted = true;
}
}
}
}
}
return nodeConverted;
}
static bool UpVersionAnimationData(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement)
{
if (classElement.GetVersion() == 0)
{
// upgrade V0 to V1 - change "Position", "Rotation", "Scale" anim params in Transform Component Nodes from AnimParamType::ByString to
// AnimParamType::Position, AnimParamType::Rotation, AnimParamType::Scale respectively
int serializedAnimStringIdx = classElement.FindElement(AZ::Crc32("SerializedString"));
if (serializedAnimStringIdx == -1)
{
AZ_Error("Serialization", false, "Failed to find 'SerializedString' element.");
return false;
}
AZStd::string serializedAnimString;
classElement.GetSubElement(serializedAnimStringIdx).GetData(serializedAnimString);
const char* buffer = serializedAnimString.c_str();
size_t size = serializedAnimString.length();
if (size > 0)
{
XmlNodeRef xmlArchive = gEnv->pSystem->LoadXmlFromBuffer(buffer, size);
// recursively traverse and convert through all nodes
if (convertTransformXMLNodes(xmlArchive))
{
// if a node was converted, replace the classElement Data with the converted XML
serializedAnimString = xmlArchive->getXML();
classElement.GetSubElement(serializedAnimStringIdx).SetData(context, serializedAnimString);
}
}
}
return true;
}
} // namespace ClassConverters
} // namespace Maestro