Explorar o código

[Track View] Add string track support, - fixing issue #9044. (#18712)

* Add new CStringTrack support
* Add CStringKeyUIControls to edit keys "On Spot" and in "Key" tab of TrackViewDialog
* Remove debugging pragma
* Update SequenceComponentBus.h

Get rid of platform-dependent and in fact unneeded azsscanf(. "%s",...)
* Update TrackViewDopeSheetBase.cpp
Trim spaces in input string

* Address PR comment
* Add missing header file + change include order - to fix Linux / clang++ compilation

I think that Linux / clang++ compilation errors were introduced after a seemingly logical change in order of includes in StringKeyUIControls.cpp, because of missing #include "EditorDefs.h" in KeyUIControls.h (and putting includes there in  #if !defined(Q_MOC_RUN)) / and  #endif section).
---------

Signed-off-by: Aleks Starykh <[email protected]>
Aleks Starykh hai 5 meses
pai
achega
997ab84b79

+ 31 - 0
Code/Editor/TrackView/KeyUIControls.h

@@ -12,6 +12,7 @@
 #include <AzFramework/Components/CameraBus.h>
 #include <CryCommon/Maestro/Types/AnimParamType.h>
 #include <CryCommon/Maestro/Types/AnimValueType.h>
+#include "EditorDefs.h"
 #include "TrackViewKeyPropertiesDlg.h"
 #endif
 
@@ -640,3 +641,33 @@ private:
         return addEventString;
     }
 };
+
+//////////////////////////////////////////////////////////////////////////
+class CStringKeyUIControls
+    : public CTrackViewKeyUIControls
+{
+public:
+    CSmartVariableArray mv_table;
+    CSmartVariableEnum<QString> mv_value;
+
+    void OnCreateVars() override
+    {
+        AddVariable(mv_table, "Key Properties");
+        AddVariable(mv_table, mv_value, "Value");
+    }
+    bool SupportTrackType([[maybe_unused]] const CAnimParamType& paramType, [[maybe_unused]] EAnimCurveType trackType, AnimValueType valueType) const override
+    {
+        return valueType == AnimValueType::String;
+    }
+    bool OnKeySelectionChange(const CTrackViewKeyBundle& selectedKeys) override;
+    void OnUIChange(IVariable* pVar, CTrackViewKeyBundle& selectedKeys) override;
+
+    unsigned int GetPriority() const override { return 1; }
+
+    static const GUID& GetClassID()
+    {
+        // {E056F001-A2DF-4E87-8DBB-980A651940DC}
+        static const GUID guid = { 0xe056f001, 0xa2df, 0x4e87, { 0x8d, 0xbb, 0x98, 0x0a, 0x65, 0x19, 0x40, 0xdc } };
+        return guid;
+    }
+};

+ 78 - 0
Code/Editor/TrackView/StringKeyUIControls.cpp

@@ -0,0 +1,78 @@
+/*
+ * 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 "EditorDefs.h"
+
+#include "KeyUIControls.h"
+
+#include "TrackViewKeyPropertiesDlg.h"
+#include <CryCommon/Maestro/Types/AnimValueType.h>
+
+bool CStringKeyUIControls::OnKeySelectionChange(const CTrackViewKeyBundle& selectedKeys)
+{
+    if (!selectedKeys.AreAllKeysOfSameType())
+    {
+        return false;
+    }
+
+    if (selectedKeys.GetKeyCount() == 1)
+    {
+        const CTrackViewKeyHandle& keyHandle = selectedKeys.GetKey(0);
+
+        AnimValueType valueType = keyHandle.GetTrack()->GetValueType();
+        if (valueType == AnimValueType::String)
+        {
+            IStringKey stringKey;
+            keyHandle.GetKey(&stringKey);
+            mv_value = QString::fromStdString(stringKey.m_strValue.c_str());            
+            return true;        
+        }
+    }
+    return false;
+}
+
+void CStringKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& selectedKeys)
+{
+    CTrackViewSequence* sequence = GetIEditor()->GetAnimation()->GetSequence();
+
+    if (!sequence || !selectedKeys.AreAllKeysOfSameType())
+    {
+        return;
+    }
+
+    for (unsigned int keyIndex = 0; keyIndex < selectedKeys.GetKeyCount(); ++keyIndex)
+    {
+        CTrackViewKeyHandle keyHandle = selectedKeys.GetKey(keyIndex);
+        if (keyHandle.GetTrack()->GetValueType() != AnimValueType::String)
+        {
+            continue;
+        }
+
+        IStringKey stringKey;
+        keyHandle.GetKey(&stringKey);
+
+        if (pVar == mv_value.GetVar())
+        {
+            stringKey.m_strValue = qUtf8Printable(mv_value);
+        }
+
+        bool isDuringUndo = false;
+        AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(isDuringUndo, &AzToolsFramework::ToolsApplicationRequests::Bus::Events::IsDuringUndoRedo);
+
+        if (isDuringUndo)
+        {
+            keyHandle.SetKey(&stringKey);
+        }
+        else
+        {
+            AzToolsFramework::ScopedUndoBatch undoBatch("Set Key Value");
+            keyHandle.SetKey(&stringKey);
+            undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId());
+        }
+    }
+}

+ 83 - 0
Code/Editor/TrackView/TrackViewDopeSheetBase.cpp

@@ -13,6 +13,7 @@
 
 // Qt
 #include <QColor>
+#include <QInputDialog>
 #include <QMenu>
 #include <QPainter>
 #include <QScrollBar>
@@ -608,6 +609,11 @@ void CTrackViewDopeSheetBase::OnLButtonDblClk(Qt::KeyboardModifiers modifiers, c
                 // bring up color picker
                 EditSelectedColorKey(pTrack);
             }
+            else if (pTrack->GetValueType() == AnimValueType::String)
+            {
+                // bring up line editor
+                EditSelectedStringKey(pTrack);
+            }
             else if (pTrack->GetValueType() != AnimValueType::Bool)
             {
                 // Edit On Spot is blank (not useful) for boolean tracks so we disable dbl-clicking to bring it up for boolean tracks
@@ -2094,6 +2100,77 @@ void CTrackViewDopeSheetBase::EditSelectedColorKey(CTrackViewTrack* pTrack)
     }
 }
 
+bool CTrackViewDopeSheetBase::CreateStringKey(CTrackViewTrack* pTrack, float keyTime)
+{
+    const auto sequence = pTrack->GetSequence();
+    const bool canCreateKey = IsOkToAddKeyHere(pTrack, keyTime);
+    if (!(sequence && canCreateKey))
+    {
+        return false;
+    }
+
+    AZStd::string str;
+    pTrack->GetValue(keyTime, str); // get previous key value or default value for the track
+
+    bool isOk;
+    const QString title(pTrack->GetParameterType().GetName());
+    const QString label(tr("Value :\t\t\t\t\t\t\t\t")); // trick to widen widget
+    const QString prevStr(str.c_str());
+    const QString newStr = QInputDialog::getText(this, title, label, QLineEdit::EchoMode::Normal, prevStr, &isOk);
+    if (!isOk)
+    {
+        return false;
+    }
+
+    CTrackViewSequenceNotificationContext context(sequence);
+    AzToolsFramework::ScopedUndoBatch undoBatch("Create Key");
+
+    CTrackViewKeyHandle newKey = pTrack->CreateKey(keyTime);
+    IStringKey strKey;
+    newKey.GetKey(&strKey);
+    strKey.m_strValue = newStr.simplified().toStdString().c_str(); // set the new value
+    newKey.SetKey(&strKey);
+
+    undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId());
+
+    return true;
+}
+
+void CTrackViewDopeSheetBase::EditSelectedStringKey(CTrackViewTrack* pTrack)
+{
+    const auto sequence = pTrack->GetSequence();
+    CTrackViewKeyBundle selectedKeyBundle = pTrack->GetSelectedKeys();
+    if (!sequence || (selectedKeyBundle.GetKeyCount() < 1))
+    {
+        return;
+    }
+
+    auto selectedKeyHandle = selectedKeyBundle.GetKey(0);
+
+    AZStd::string str;
+    pTrack->GetValue(selectedKeyHandle.GetTime(), str); // get selected key value
+
+    bool isOk;
+    const QString title(pTrack->GetParameterType().GetName());
+    const QString label(tr("Value :\t\t\t\t\t\t\t\t")); // trick to widen widget
+    const QString prevStr(str.c_str());
+    const QString newStr = QInputDialog::getText(this, title, label, QLineEdit::EchoMode::Normal, prevStr, &isOk);
+    if (!isOk)
+    {
+        return;
+    }
+    CTrackViewSequenceNotificationContext context(sequence);
+    AzToolsFramework::ScopedUndoBatch undoBatch("Set Key");
+
+    IStringKey strKey;
+    selectedKeyHandle.GetKey(&strKey);
+    strKey.m_strValue = newStr.simplified().toStdString().c_str(); // set the new value
+    selectedKeyHandle.SetKey(&strKey);
+
+    undoBatch.MarkEntityDirty(sequence->GetSequenceComponentEntityId());
+}
+
+
 void CTrackViewDopeSheetBase::AcceptUndo()
 {
     if (CUndo::IsRecording())
@@ -2213,6 +2290,12 @@ void CTrackViewDopeSheetBase::AddKeys(const QPoint& point, const bool bTryAddKey
         }
         else if (pTrack->GetChildCount() == 0)          // A simple track
         {
+            if (pTrack->GetValueType() == AnimValueType::String)
+            {
+                CreateStringKey(pTrack, keyTime);
+                return;
+            }
+
             if (IsOkToAddKeyHere(pTrack, keyTime))
             {
                 AzToolsFramework::ScopedUndoBatch undoBatch("Create Key");

+ 3 - 0
Code/Editor/TrackView/TrackViewDopeSheetBase.h

@@ -204,6 +204,9 @@ private:
     bool CreateColorKey(CTrackViewTrack* pTrack, float keyTime);
     void EditSelectedColorKey(CTrackViewTrack* pTrack);
 
+    bool CreateStringKey(CTrackViewTrack* pTrack, float keyTime);
+    void EditSelectedStringKey(CTrackViewTrack* pTrack);
+
     void AcceptUndo();
 
     // Returns the snapping mode modified active keys

+ 1 - 0
Code/Editor/TrackView/TrackViewKeyPropertiesDlg.cpp

@@ -72,6 +72,7 @@ CTrackViewKeyPropertiesDlg::CTrackViewKeyPropertiesDlg(QWidget* hParentWnd)
     m_keyControls.push_back(new CSelectKeyUIControls());
     m_keyControls.push_back(new CSequenceKeyUIControls());
     m_keyControls.push_back(new CSoundKeyUIControls());
+    m_keyControls.push_back(new CStringKeyUIControls());
     m_keyControls.push_back(new CTimeRangeKeyUIControls());
     m_keyControls.push_back(new CTrackEventKeyUIControls());
 

+ 7 - 0
Code/Editor/TrackView/TrackViewPythonFuncs.cpp

@@ -486,6 +486,13 @@ namespace
             return AZStd::make_any<AZ::Color>(value.GetX(), value.GetY(), value.GetZ(), 0.0f);
         }
         break;
+        case AnimValueType::String:
+            {
+                AZStd::string value;
+                pTrack->GetValue(time, value);
+                return AZStd::make_any<AZStd::string>(value);
+            }
+            break;
         default:
             throw std::runtime_error("Unsupported key type");
         }

+ 1 - 0
Code/Editor/editor_lib_files.cmake

@@ -497,6 +497,7 @@ set(FILES
     TrackView/SelectKeyUIControls.cpp
     TrackView/SequenceKeyUIControls.cpp
     TrackView/SoundKeyUIControls.cpp
+    TrackView/StringKeyUIControls.cpp
     TrackView/TrackEventKeyUIControls.cpp
     TrackView/TrackViewTrackPropsDlg.ui
     TrackView/TVEventsDialog.cpp

+ 16 - 0
Code/Legacy/CryCommon/AnimKey.h

@@ -528,6 +528,21 @@ struct IScreenFaderKey
     EFadeChangeType m_fadeChangeType;
 };
 
+/** IStringKey used in string tracks.
+ */
+struct IStringKey
+    : public IKey
+{
+    AZStd::string m_strValue;
+
+    IStringKey() = default;
+
+    IStringKey(const AZStd::string value)
+        : m_strValue(value) 
+    {
+    }
+};
+
 namespace AZ
 {
     AZ_TYPE_INFO_SPECIALIZE(IKey, "{680BD51E-C106-4BBF-9A6F-CD551E00519F}");
@@ -544,5 +559,6 @@ namespace AZ
     AZ_TYPE_INFO_SPECIALIZE(ISequenceKey, "{B55294AD-F14E-43AC-B6B5-AC27B377FE00}");
     AZ_TYPE_INFO_SPECIALIZE(ISoundKey, "{452E50CF-B7D0-42D5-A86A-B295682674BB}");
     AZ_TYPE_INFO_SPECIALIZE(ITimeRangeKey, "{17807C95-C7A1-481B-AD94-C54D83928D0B}");
+    AZ_TYPE_INFO_SPECIALIZE(IStringKey, "{A35D94C2-776B-4BA7-BBBC-1A1FD4402023}");
 }
 #endif // CRYINCLUDE_CRYCOMMON_ANIMKEY_H

+ 9 - 7
Code/Legacy/CryCommon/IMovieSystem.h

@@ -264,7 +264,7 @@ struct IAnimTrack
 
     //////////////////////////////////////////////////////////////////////////
     virtual EAnimCurveType GetCurveType() = 0;
-    virtual AnimValueType     GetValueType() = 0;
+    virtual AnimValueType  GetValueType() = 0;
 
 #ifdef MOVIESYSTEM_SUPPORT_EDITING
     // This color is used for the editor.
@@ -288,7 +288,7 @@ struct IAnimTrack
     // Animation track can contain sub-tracks (Position XYZ anim track have sub-tracks for x,y,z)
     // Get count of sub tracks.
     virtual int GetSubTrackCount() const = 0;
-    // Retrieve pointer the specfied sub track.
+    // Retrieve pointer the specified sub track.
     virtual IAnimTrack* GetSubTrack(int nIndex) const = 0;
     virtual AZStd::string GetSubTrackName(int nIndex) const = 0;
     virtual void SetSubTrackName(int nIndex, const char* name) = 0;
@@ -378,6 +378,7 @@ struct IAnimTrack
     virtual void GetValue(float time, AZ::Quaternion& value) = 0;
     virtual void GetValue(float time, bool& value) = 0;
     virtual void GetValue(float time, Maestro::AssetBlends<AZ::Data::AssetData>& value) = 0;
+    virtual void GetValue(float time, AZStd::string& value) = 0;
 
     //////////////////////////////////////////////////////////////////////////
     // Set track value at specified time.
@@ -389,6 +390,7 @@ struct IAnimTrack
     virtual void SetValue(float time, const AZ::Quaternion& value, bool bDefault = false) = 0;
     virtual void SetValue(float time, const bool& value, bool bDefault = false) = 0;
     virtual void SetValue(float time, const Maestro::AssetBlends<AZ::Data::AssetData>& value, bool bDefault = false) = 0;
+    virtual void SetValue(float time, const AZStd::string& value, bool bDefault = false) = 0;
 
     // Only for position tracks, offset all track keys by this amount.
     virtual void OffsetKeyPosition(const AZ::Vector3& value) = 0;
@@ -419,7 +421,7 @@ struct IAnimTrack
     virtual bool IsSortMarkerKey([[maybe_unused]] unsigned int keyIndex) const { return false; }
 
     //! Return the index of the key which lies right after the given key in time.
-    //! @param key Index of of key.
+    //! @param key Index of key.
     //! @return Index of the next key in time. If the last key given, this returns -1.
     // In case of keys sorted, it's just 'key+1', but if not sorted, it can be another value.
     virtual int NextKeyByTime(int key) const;
@@ -429,9 +431,9 @@ struct IAnimTrack
     //! Set the animation layer index. (only for character/look-at tracks ATM)
     virtual void SetAnimationLayerIndex([[maybe_unused]] int index) { }
 
-    //! Returns whether the track responds to muting (false by default), which only affects the Edtior.
+    //! Returns whether the track responds to muting (false by default), which only affects the Editor.
     //! Tracks that use mute should override this, such as CSoundTrack
-    //! @return Boolean of whether the track respnnds to muting or not
+    //! @return Boolean of whether the track responds to muting or not
     virtual bool UsesMute() const { return false; }
 
     //! Set a multiplier which will be multiplied to track values in SetValue and divided out in GetValue if requested
@@ -492,8 +494,8 @@ public:
 
         AZStd::string name;           // parameter name.
         CAnimParamType paramType;     // parameter id.
-        AnimValueType valueType;       // value type, defines type of track to use for animating this parameter.
-        ESupportedParamFlags flags; // combination of flags from ESupportedParamFlags.
+        AnimValueType valueType;      // value type, defines type of track to use for animating this parameter.
+        ESupportedParamFlags flags;   // combination of flags from ESupportedParamFlags.
     };
 
     using AnimParamInfos = AZStd::vector<SParamInfo>;

+ 283 - 5
Code/Legacy/CryCommon/Maestro/Bus/SequenceComponentBus.h

@@ -65,6 +65,7 @@ namespace Maestro
         class AnimatedBoolValue;
         class AnimatedQuaternionValue;
         class AnimatedAssetIdValue;
+        class AnimatedStringValue;
 
         class AnimatedValue
         {
@@ -104,6 +105,10 @@ namespace Maestro
             {
                 assetIdValue = GetAssetIdValue();
             }
+            void GetValue(AZStd::string& stringValue) const
+            {
+                stringValue = GetStringValue();
+            }
 
             // same as above but returning the value
             virtual AZ::Quaternion GetQuaternionValue() const = 0;
@@ -113,6 +118,7 @@ namespace Maestro
             virtual AZ::s32     GetS32Value() const = 0;
             virtual AZ::u32     GetU32Value() const = 0;
             virtual const AZ::Data::AssetId& GetAssetIdValue() const = 0;
+            virtual AZStd::string GetStringValue() const = 0;
 
             // Set the value to the given arg. Returns true if the arg is the 'native' type of the concrete animated value
             virtual bool SetValue(const AZ::Vector3& vector3Value) = 0;
@@ -122,12 +128,14 @@ namespace Maestro
             virtual bool SetValue(AZ::s32 s32Value) = 0;
             virtual bool SetValue(AZ::u32 u32Value) = 0;
             virtual bool SetValue(const AZ::Data::AssetId& assetIdValue) = 0;
+            virtual bool SetValue(const AZStd::string& stringValue) = 0;
 
             virtual bool IsClose(const AnimatedFloatValue& rhs, float tolerance = AZ::Constants::Tolerance) const = 0;
             virtual bool IsClose(const AnimatedVector3Value& rhs, float tolerance = AZ::Constants::Tolerance) const = 0;
             virtual bool IsClose(const AnimatedQuaternionValue& rhs, float tolerance = AZ::Constants::Tolerance) const = 0;
             virtual bool IsClose(const AnimatedBoolValue& rhs, float tolerance = AZ::Constants::Tolerance) const = 0;
             virtual bool IsClose(const AnimatedAssetIdValue& rhs, float tolerance = AZ::Constants::Tolerance) const = 0;
+            virtual bool IsClose(const AnimatedStringValue& rhs, float tolerance = AZ::Constants::Tolerance) const = 0;
 
         protected:
             AnimatedValue() {}     // protected constructor as the interface should never be constructed directly - it's an abstract class
@@ -177,6 +185,12 @@ namespace Maestro
                 static AZ::Data::AssetId assetId;
                 return assetId;
             }
+            AZStd::string GetStringValue() const override
+            {
+                char buffer[33] = { 0 };
+                azsprintf(buffer, "%f", m_value);
+                return AZStd::string(buffer);
+            }
 
             bool SetValue(const AZ::Vector3& vector3Value) override
             {
@@ -213,6 +227,15 @@ namespace Maestro
                 AZ_UNUSED(assetIdValue);
                 return true;
             }
+            bool SetValue(const AZStd::string& stringValue) override
+            {
+                float f = 0;
+                if (azsscanf(stringValue.c_str(), "%f", &f) == 1)
+                {
+                    m_value = f; 
+                }
+                return false;
+            }
 
             bool IsClose(const AnimatedFloatValue& rhs, float tolerance = AZ::Constants::FloatEpsilon) const override
             {
@@ -232,11 +255,15 @@ namespace Maestro
             }
             bool IsClose(const AnimatedAssetIdValue& rhs, float tolerance = AZ::Constants::Tolerance) const override
             {
-                AZ_Assert(0, "Shouldnt be used.");
+                AZ_Assert(0, "Shouldn't be used.");
                 AZ_UNUSED(rhs);
                 AZ_UNUSED(tolerance);
                 return false;
             }
+            bool IsClose(const AnimatedStringValue& rhs, float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return AZ::IsClose(m_value, rhs.GetFloatValue(), tolerance);
+            }
 
         private:
             float m_value;
@@ -287,6 +314,12 @@ namespace Maestro
                 static AZ::Data::AssetId assetId;
                 return assetId;
             }
+            AZStd::string GetStringValue() const override
+            {
+                char buffer[97] = { 0 };
+                azsprintf(buffer, "%f,%f,%f", m_value.GetX(), m_value.GetY(), m_value.GetZ());
+                return AZStd::string(buffer);
+            }
 
             bool SetValue(const AZ::Vector3& vector3Value) override
             {
@@ -320,8 +353,20 @@ namespace Maestro
             }
             bool SetValue(const AZ::Data::AssetId& assetIdValue) override
             {
+                AZ_Assert(0, "Shouldn't be used.");
                 AZ_UNUSED(assetIdValue);
-                return true;
+                return false;
+            }
+            bool SetValue(const AZStd::string& stringValue) override
+            {
+                float x = 0;
+                float y = 0;
+                float z = 0;
+                if (azsscanf(stringValue.c_str(), "%f,%f,%f", &x, &y, &z) == 3)
+                {
+                    m_value = AZ::Vector3(x, y, z);
+                }
+                return false;
             }
 
             bool IsClose(const AnimatedFloatValue& rhs, float tolerance = AZ::Constants::FloatEpsilon) const override
@@ -347,6 +392,10 @@ namespace Maestro
                 AZ_UNUSED(tolerance);
                 return false;
             }
+            bool IsClose(const AnimatedStringValue& rhs, float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return m_value.IsClose(rhs.GetVector3Value(), tolerance);
+            }
 
         private:
             AZ::Vector3 m_value;
@@ -398,6 +447,12 @@ namespace Maestro
                 static AZ::Data::AssetId assetId;
                 return assetId;
             }
+            AZStd::string GetStringValue() const override
+            {
+                char buffer[129] = { 0 };
+                azsprintf(buffer, "%f,%f,%f,%f", m_value.GetX(), m_value.GetY(), m_value.GetZ(), m_value.GetW());
+                return AZStd::string(buffer);
+            }
 
             bool SetValue(const AZ::Vector3& vector3Value) override
             {
@@ -432,8 +487,21 @@ namespace Maestro
             }
             bool SetValue(const AZ::Data::AssetId& assetIdValue) override
             {
+                AZ_Assert(0, "Shouldn't be used.");
                 AZ_UNUSED(assetIdValue);
-                return true;
+                return false;
+            }
+            bool SetValue(const AZStd::string& stringValue) override
+            {
+                float x = 0;
+                float y = 0;
+                float z = 0;
+                float w = 0;
+                if (azsscanf(stringValue.c_str(), "%f,%f,%f,%f", &x, &y, &z, &w) == 4)
+                {
+                    m_value = AZ::Quaternion(x, y, z, w);
+                }
+                return false;
             }
 
             bool IsClose(const AnimatedFloatValue& rhs, float tolerance = AZ::Constants::FloatEpsilon) const override
@@ -459,6 +527,10 @@ namespace Maestro
                 AZ_UNUSED(tolerance);
                 return false;
             }
+            bool IsClose(const AnimatedStringValue& rhs, float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return m_value.IsClose(rhs.GetQuaternionValue(), tolerance);
+            }
 
         private:
             AZ::Quaternion m_value;
@@ -508,6 +580,12 @@ namespace Maestro
                 static AZ::Data::AssetId assetId;
                 return assetId;
             }
+            AZStd::string GetStringValue() const override
+            {
+                char buffer[2] = { 0 };
+                azsprintf(buffer, "%d", m_value ? 1 : 0); // 1 or 0 representation
+                return AZStd::string(buffer);
+            }
 
             bool SetValue(const AZ::Vector3& vector3Value) override
             {
@@ -541,8 +619,19 @@ namespace Maestro
             }
             bool SetValue(const AZ::Data::AssetId& assetIdValue) override
             {
+                AZ_Assert(0, "Shouldn't be used.");
                 AZ_UNUSED(assetIdValue);
-                return true;
+                return false;
+            }
+            bool SetValue(const AZStd::string& stringValue) override
+            {
+                int i = 0;
+                if (azsscanf(stringValue.c_str(), "%d", &i) == 1) // 1 or 0 representation ?
+                {
+                    return i > 0;
+                }
+                // "true" or "false" representation ?
+                return azstricmp(stringValue.c_str(), "true") == 0;
             }
 
             bool IsClose(const AnimatedFloatValue& rhs, float tolerance = AZ::Constants::FloatEpsilon) const override
@@ -569,6 +658,10 @@ namespace Maestro
                 AZ_UNUSED(tolerance);
                 return false;
             }
+            bool IsClose(const AnimatedStringValue& rhs, [[maybe_unused]] float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return m_value == rhs.GetBoolValue();
+            }
 
         private:
             bool m_value;
@@ -616,6 +709,10 @@ namespace Maestro
             {
                 return m_value;
             }
+            AZStd::string GetStringValue() const override
+            {
+                return m_value.ToString<AZStd::string>();
+            }
 
             bool SetValue(const AZ::Vector3& vector3Value) override
             {
@@ -652,6 +749,11 @@ namespace Maestro
                 m_value = assetIdValue;
                 return true;
             }
+            bool SetValue(const AZStd::string& stringValue) override
+            {
+                m_value = AZ::Data::AssetId::CreateString(stringValue);
+                return false;
+            }
 
             bool IsClose([[maybe_unused]] const AnimatedFloatValue& rhs, float tolerance = AZ::Constants::FloatEpsilon) const override
             {
@@ -677,11 +779,187 @@ namespace Maestro
             {
                 return m_value == rhs.m_value;
             }
+            bool IsClose(const AnimatedStringValue& rhs, [[maybe_unused]] float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return m_value == rhs.GetAssetIdValue();
+            }
 
         private:
             AZ::Data::AssetId m_value;
         };
 
+        class AnimatedStringValue : public AnimatedValue
+        {
+        public:
+            AZ_TYPE_INFO(AnimatedStringValue, "{B2DEEE6F-6055-4FC3-BA75-4B263EA59A5A}");
+
+            AnimatedStringValue(const AZStd::string& value)
+            {
+                m_value = value;
+            }
+            AnimatedStringValue() = default;
+            ~AnimatedStringValue()
+            {
+            }
+
+            AZ::TypeId GetTypeId() const override
+            {
+                return AZ::AzTypeInfo<AZStd::string>::Uuid();
+            }
+
+            float GetFloatValue() const override
+            {
+                float f = 0;
+                if (azsscanf(m_value.c_str(), "%f", &f) == 1)
+                {
+                    return f;
+                }
+                return 0.0f;
+            }
+            AZ::Vector3 GetVector3Value() const override
+            {
+                float x = 0;
+                float y = 0;
+                float z = 0;
+                if (azsscanf(m_value.c_str(), "%f,%f,%f", &x, &y, &z) == 3)
+                {
+                    return AZ::Vector3(x, y, z);
+                }
+
+                return AZ::Vector3::CreateZero();
+            }
+            AZ::Quaternion GetQuaternionValue() const override
+            {
+                float x = 0;
+                float y = 0;
+                float z = 0;
+                float w = 0;
+                if (azsscanf(m_value.c_str(), "%f,%f,%f,%f", &x, &y, &z, &w) == 4)
+                {
+                    return AZ::Quaternion(x, y, z, w);
+                }
+                return AZ::Quaternion::CreateIdentity();
+            }
+            bool GetBoolValue() const override
+            {
+                int i = 0;
+                if (azsscanf(m_value.c_str(), "%d", &i) == 1) // 1 or 0 representation ?
+                {
+                    return i > 0;
+                }
+                // "true" or "false" representation ?
+                return azstricmp(m_value.c_str(), "true") == 0;
+            }
+            AZ::s32 GetS32Value() const override
+            {
+                AZ::s32 s = 0;
+                if (azsscanf(m_value.c_str(), "%d", &s) == 1)
+                {
+                    return s;
+                }
+                return 0;
+            }
+            AZ::u32 GetU32Value() const override
+            {
+                AZ::u32 u = 0;
+                if (azsscanf(m_value.c_str(), "%u", &u) == 1)
+                {
+                    return u;
+                }
+                return 0;
+            }
+            const AZ::Data::AssetId& GetAssetIdValue() const override
+            {
+                static AZ::Data::AssetId assetId = AZ::Data::AssetId::CreateString(m_value);
+                return assetId;
+            }
+            AZStd::string GetStringValue() const override
+            {
+                return m_value;
+            }
+
+            bool SetValue(const AZ::Vector3& vector3Value) override
+            {
+                char buffer[97] = { 0 };
+                azsprintf(buffer, "%f,%f,%f", vector3Value.GetX(), vector3Value.GetY(), vector3Value.GetZ());
+                m_value = buffer;
+                return false;
+            }
+            bool SetValue(const AZ::Quaternion& quaternionValue) override
+            {
+                char buffer[129] = { 0 };
+                azsprintf(buffer, "%f,%f,%f,%f", quaternionValue.GetX(), quaternionValue.GetY(), quaternionValue.GetZ(), quaternionValue.GetW());
+                m_value = buffer;
+                return false;
+            }
+            bool SetValue(float floatValue) override
+            {
+                char buffer[33] = { 0 };
+                azsprintf(buffer, "%f", floatValue);
+                m_value = buffer;
+                return false;
+            }
+            bool SetValue(bool boolValue) override
+            {
+                char buffer[2] = { 0 };
+                azsprintf(buffer, "%d", boolValue ? 1 : 0);
+                m_value = buffer;
+                return false;
+            }
+            bool SetValue(AZ::s32 s32Value) override
+            {
+                char buffer[33] = { 0 };
+                azsprintf(buffer, "%d", s32Value);
+                m_value = buffer;
+                return false;
+            }
+            bool SetValue(AZ::u32 u32Value) override
+            {
+                char buffer[33] = { 0 };
+                azsprintf(buffer, "%u", u32Value);
+                m_value = buffer;
+                return false;
+            }
+            bool SetValue(const AZ::Data::AssetId& assetIdValue) override
+            {
+                assetIdValue.ToString(m_value, AZ::Data::AssetId::SubIdDisplayType::Decimal);
+                return false;
+            }
+            bool SetValue(const AZStd::string& stringValue) override
+            {
+                m_value = stringValue;
+                return true;
+            }
+
+            bool IsClose(const AnimatedFloatValue& rhs, float tolerance = AZ::Constants::FloatEpsilon) const override
+            {
+                return AZ::IsClose(GetFloatValue(), rhs.GetFloatValue(), tolerance);
+            }
+            bool IsClose(const AnimatedVector3Value& rhs, float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return GetVector3Value().IsClose(rhs.GetVector3Value(), tolerance);
+            }
+            bool IsClose(const AnimatedQuaternionValue& rhs, float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return GetQuaternionValue().IsClose(rhs.GetQuaternionValue(), tolerance);
+            }
+            bool IsClose(const AnimatedBoolValue& rhs, [[maybe_unused]] float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return GetBoolValue() == rhs.GetBoolValue();
+            }
+            bool IsClose(const AnimatedAssetIdValue& rhs, [[maybe_unused]] float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return GetAssetIdValue() == rhs.GetAssetIdValue();
+            }
+            bool IsClose(const AnimatedStringValue& rhs, [[maybe_unused]] float tolerance = AZ::Constants::Tolerance) const override
+            {
+                return m_value == rhs.m_value;
+            }
+
+        private:
+            AZStd::string m_value;
+        };
+
         //////////////////////////////////////////////////////////////////////////
         // EBusTraits overrides - application is a singleton
         static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;  // Only one component on a entity can implement the events
@@ -710,7 +988,7 @@ namespace Maestro
 
         /**
          * Track View will expect some components to supply a GetAssetDuration event so Track View can query the duration of an asset (like a motion)
-         * without having any knowledge of that that asset is.
+         * without having any knowledge of what that asset is.
          */
         virtual void GetAssetDuration(AnimatedValue& returnValue, const AZ::EntityId& animatedEntityId, AZ::ComponentId componentId, const AZ::Data::AssetId& assetId) = 0;
 

+ 1 - 0
Code/Legacy/CryCommon/Maestro/Types/AnimValueType.h

@@ -32,6 +32,7 @@ enum class AnimValueType
     RGB = 20,
     CharacterAnim = 21,
     AssetBlend = 22,
+    String = 23,
 
     Unknown = static_cast<int>(0xFFFFFFFF)
 };

+ 71 - 2
Gems/Maestro/Code/Source/Cinematics/AnimComponentNode.cpp

@@ -228,6 +228,26 @@ namespace Maestro
         return retNumKeysSet;
     }
 
+    int CAnimComponentNode::SetKeysForChangedStringTrackValue(IAnimTrack* track, [[maybe_unused]] int keyIdx, float time)
+    {
+        int retNumKeysSet = 0;
+        AZStd::string currTrackValue;
+        track->GetValue(time, currTrackValue);
+        SequenceComponentRequests::AnimatedStringValue currValue(currTrackValue);
+        SequenceComponentRequests::AnimatablePropertyAddress animatableAddress(m_componentId, track->GetParameterType().GetName());
+        SequenceComponentRequestBus::Event(m_pSequence->GetSequenceEntityId(), &SequenceComponentRequestBus::Events::GetAnimatedPropertyValue,
+            currValue,GetParentAzEntityId(), animatableAddress);
+        AZStd::string currStringValue;
+        currValue.GetValue(currStringValue);
+
+        if (currTrackValue != currStringValue)
+        {
+            track->SetValue(time, currStringValue, false);
+            retNumKeysSet++;
+        }
+        return retNumKeysSet;
+    }
+
     int CAnimComponentNode::SetKeysForChangedTrackValues(float time)
     {
         int retNumKeysSet = 0;
@@ -258,7 +278,10 @@ namespace Maestro
                 case AnimValueType::Vector4:
                     AZ_Warning("TrackView", false, "Vector4's are not supported for recording.");
                     break;
-            }
+                case AnimValueType::String:
+                    retNumKeysSet += SetKeysForChangedStringTrackValue(track, keyIdx, time);
+                    break;
+                }
         }
 
         return retNumKeysSet;
@@ -721,6 +744,10 @@ namespace Maestro
             {
                 propertyInfo.m_animNodeParamInfo.valueType = AnimValueType::AssetBlend;
             }
+            else if (propertyTypeId == AZ::AzTypeInfo<AZStd::string>::Uuid())
+            {
+                propertyInfo.m_animNodeParamInfo.valueType = AnimValueType::String;
+            }
             // the fall-through default type is propertyInfo.m_animNodeParamInfo.valueType = AnimValueType::Float
         }
 
@@ -834,7 +861,9 @@ namespace Maestro
                         defaultValue.GetValue(vector3Value);
                         vector3Value = vector3Value.GetClamp(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne());
 
-                        pTrack->SetValue(0, vector3Value, /*setDefault=*/ true, /*applyMultiplier=*/ true);
+                        constexpr const bool setDefault = true;
+                        constexpr const bool applyMultiplier = true;
+                        pTrack->SetValue(0, vector3Value, setDefault, applyMultiplier);
                         break;
                     }
                     case AnimValueType::Bool:
@@ -853,6 +882,18 @@ namespace Maestro
                         pTrack->SetValue(0, assetData, true);
                         break;
                     }
+                    case AnimValueType::String:
+                    {
+                        SequenceComponentRequests::AnimatedStringValue defaultValue;
+                        AZStd::string stringValue;
+                        SequenceComponentRequestBus::Event(m_pSequence->GetSequenceEntityId(), &SequenceComponentRequestBus::Events::GetAnimatedPropertyValue,
+                            defaultValue, GetParentAzEntityId(), address);
+                        defaultValue.GetValue(stringValue);
+
+                        constexpr const bool setDefault = true;
+                        pTrack->SetValue(0, stringValue, setDefault);
+                        break;
+                    }
                     default:
                     {
                         AZ_Warning("TrackView", false, "Unsupported value type requested for Component Node Track %s, skipping...", paramType.GetName());
@@ -1039,6 +1080,34 @@ namespace Maestro
                                 }
                                 break;
                             }
+                            case AnimValueType::String:
+                            {
+                                AZStd::string str;
+                                pTrack->GetValue(ac.time, str);
+
+                                SequenceComponentRequests::AnimatedStringValue value(str);
+                                SequenceComponentRequests::AnimatedStringValue prevValue(str);
+                                bool wasInvoked = false;
+                                const AZ::EntityId& sequenceEntityId = m_pSequence->GetSequenceEntityId();
+                                SequenceComponentRequestBus::EventResult(wasInvoked, sequenceEntityId, &SequenceComponentRequestBus::Events::GetAnimatedPropertyValue,
+                                    prevValue, GetParentAzEntityId(), animatableAddress);
+
+                                if (!wasInvoked)
+                                {
+                                    AZ_Trace("CAnimComponentNode::Animate", "GetAnimatedPropertyValue failed for %s", sequenceEntityId.ToString().c_str());
+                                }
+
+                                AZStd::string stringPrevValue;
+                                prevValue.GetValue(stringPrevValue);
+
+                                if (!value.IsClose(prevValue))
+                                {
+                                    // only set the value if it's changed
+                                    SequenceComponentRequestBus::Event(m_pSequence->GetSequenceEntityId(), &SequenceComponentRequestBus::Events::SetAnimatedPropertyValue,
+                                        GetParentAzEntityId(), animatableAddress, value);
+                                }
+                                break;
+                            }
                             default:
                             {
                                 AZ_Warning("TrackView", false, "Unsupported value type %d requested for Component Node Track %s, skipping...", pTrack->GetValueType(), paramType.GetName());

+ 1 - 0
Gems/Maestro/Code/Source/Cinematics/AnimComponentNode.h

@@ -157,6 +157,7 @@ namespace Maestro
             bool applyTrackMultiplier = true,
             float isChangedTolerance = AZ::Constants::Tolerance);
         int SetKeysForChangedQuaternionTrackValue(IAnimTrack* track, int keyIdx, float time);
+        int SetKeysForChangedStringTrackValue(IAnimTrack* track, int keyIdx, float time);
 
         static const float s_rgbMultiplier; // standard value for tracks' multiplier with AnimValueType::RGB 
 

+ 4 - 0
Gems/Maestro/Code/Source/Cinematics/AnimNode.cpp

@@ -29,6 +29,7 @@
 #include "CaptureTrack.h"
 #include "CommentTrack.h"
 #include "ScreenFaderTrack.h"
+#include "StringTrack.h"
 #include "TimeRangesTrack.h"
 
 #include <AzCore/std/sort.h>
@@ -422,6 +423,9 @@ namespace Maestro
             case AnimValueType::AssetBlend:
                 pTrack = aznew CAssetBlendTrack;
                 break;
+            case AnimValueType::String:
+                pTrack = aznew CStringTrack;
+                break;
             }
         }
 

+ 3 - 0
Gems/Maestro/Code/Source/Cinematics/AnimSerializer.cpp

@@ -109,6 +109,9 @@ void AnimSerializer::ReflectAnimTypes(AZ::ReflectContext* context)
             ->Field("Duration", &ISoundKey::fDuration)
             ->Field("Color", &ISoundKey::customColor);
 
+        serializeContext->Class<IStringKey, IKey>()
+            ->Field("Value", &IStringKey::m_strValue);
+
         serializeContext->Class<ITimeRangeKey, IKey>()
             ->Field("Duration", &ITimeRangeKey::m_duration)
             ->Field("Start", &ITimeRangeKey::m_startTime)

+ 10 - 0
Gems/Maestro/Code/Source/Cinematics/AnimSplineTrack.h

@@ -270,6 +270,11 @@ namespace Maestro
             AZ_Assert(false, "Not expected to be used");
         }
 
+        void GetValue([[maybe_unused]] float time, [[maybe_unused]] AZStd::string& value) override
+        {
+            AZ_Assert(false, "Not expected to be used");
+        }
+
         void SetValue(float time, const float& value, bool bDefault = false, bool applyMultiplier = false) override
         {
             AZ_Assert(false, "Not expected to be used");
@@ -312,6 +317,11 @@ namespace Maestro
             AZ_Assert(false, "Not expected to be used");
         }
 
+        void SetValue([[maybe_unused]] float time, [[maybe_unused]] const AZStd::string& value, [[maybe_unused]] bool bDefault = false) override
+        {
+            AZ_Assert(false, "Not expected to be used");
+        }
+
         void OffsetKeyPosition([[maybe_unused]] const AZ::Vector3& value) override
         {
             AZ_Assert(false, "Not expected to be used");

+ 10 - 0
Gems/Maestro/Code/Source/Cinematics/AnimTrack.h

@@ -264,6 +264,11 @@ namespace Maestro
             AZ_Assert(false, "Not expected to be used");
         }
 
+        void GetValue([[maybe_unused]] float time, [[maybe_unused]] AZStd::string& value) override
+        {
+            AZ_Assert(false, "Not expected to be used");
+        }
+
         //////////////////////////////////////////////////////////////////////////
         // Set track value at specified time.
         // Adds new keys if required.
@@ -314,6 +319,11 @@ namespace Maestro
             AZ_Assert(false, "Not expected to be used");
         }
 
+        void SetValue([[maybe_unused]] float time, [[maybe_unused]] const AZStd::string& value, [[maybe_unused]] bool bDefault = false) override
+        {
+            AZ_Assert(false, "Not expected to be used");
+        }
+
         void OffsetKeyPosition([[maybe_unused]] const AZ::Vector3& value) override
         {
             AZ_Assert(false, "Not expected to be used");

+ 11 - 0
Gems/Maestro/Code/Source/Cinematics/CompoundSplineTrack.h

@@ -166,6 +166,12 @@ namespace Maestro
             AZ_Assert(false, "Not expected to be used");
         }
 
+        void GetValue([[maybe_unused]] float time, [[maybe_unused]] AZStd::string& value) override
+        {
+            AZ_Assert(false, "Not expected to be used");
+        }
+
+
         //////////////////////////////////////////////////////////////////////////
         // Set track value at specified time.
         // Adds new keys if required.
@@ -187,6 +193,11 @@ namespace Maestro
             AZ_Assert(false, "Not expected to be used");
         }
 
+        void SetValue([[maybe_unused]] float time, [[maybe_unused]] const AZStd::string& value, [[maybe_unused]] bool bDefault = false) override
+        {
+            AZ_Assert(false, "Not expected to be used");
+        }
+
         void OffsetKeyPosition(const AZ::Vector3& value) override;
         void UpdateKeyDataAfterParentChanged(const AZ::Transform& oldParentWorldTM, const AZ::Transform& newParentWorldTM) override;
 

+ 151 - 0
Gems/Maestro/Code/Source/Cinematics/StringTrack.cpp

@@ -0,0 +1,151 @@
+/*
+ * 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 "StringTrack.h"
+
+#include <AzCore/Serialization/SerializeContext.h>
+#include <CryCommon/Maestro/Types/AnimValueType.h>
+
+namespace Maestro
+{
+    AnimValueType CStringTrack::GetValueType()
+    {
+        return AnimValueType::String;
+    }
+
+    int CStringTrack::CreateKey(float time)
+    {
+        CheckValid(); // sort keys
+        const int nkey = GetNumKeys();
+        IStringKey key;
+        GetValue(time, key.m_strValue); // copy last key value or default value
+        key.time = time;
+        SetNumKeys(nkey + 1);
+        SetKey(nkey, &key);
+        Invalidate();
+
+        return nkey;
+    }
+
+    void CStringTrack::GetValue(float time, AZStd::string& value)
+    {
+        value = m_defaultValue;
+
+        CheckValid();
+
+        int nkeys = static_cast<int>(m_keys.size());
+        if (nkeys < 1)
+        {
+            return;
+        }
+
+        for (int idx = 0; idx < nkeys; ++idx)
+        {
+            if (time >= m_keys[idx].time)
+            {
+                value = m_keys[idx].m_strValue;
+            }
+        }
+    }
+
+    void CStringTrack::SetValue(float time, const AZStd::string& value, bool bDefault)
+    {
+        if (bDefault)
+        {
+            SetDefaultValue(value);
+        }
+        else if (time > m_timeRange.end)
+        {
+            return;
+        }
+        else
+        {
+            IStringKey key(value);
+            SetKeyAtTime(time, &key);
+        }
+        Invalidate();
+    }
+
+    void CStringTrack::SetKeyAtTime(float time, IKey* key)
+    {
+        if (!key)
+        {
+            AZ_Assert(false, "SetKeyAtTime given a null pointer to key.");
+            return;
+        }
+
+        AZStd::clamp(time, m_timeRange.start, m_timeRange.end);
+        key->time = time;
+
+        // Find key with given time.
+        CheckValid();
+        constexpr float MinTimePrecision = 0.01f;
+        bool found = false;
+        for (size_t i = 0; i < m_keys.size(); i++)
+        {
+            float keyt = m_keys[i].time;
+            if (AZStd::abs(keyt - time) < MinTimePrecision) // Found a close key?
+            {
+                key->flags = m_keys[i].flags; // Reserve the flag value.
+                SetKey(static_cast<int>(i), key);
+                found = true;
+                break;
+            }
+        }
+        if (!found)
+        {
+            // Key with this time not found -> create a new one.
+            int keyIndex = CreateKey(time);
+            key->flags = m_keys[keyIndex].flags; // Reserve the flag value.
+            SetKey(keyIndex, key);
+        }
+    }
+
+    void CStringTrack::GetKeyInfo([[maybe_unused]] int index, const char*& description, float& duration)
+    {
+        description = 0;
+        duration = 0;
+    }
+
+    static bool StringTrackVersionConverter(AZ::SerializeContext& serializeContext, AZ::SerializeContext::DataElementNode& rootElement)
+    {
+        if (rootElement.GetVersion() < 3)
+        {
+            rootElement.AddElement(serializeContext, "BaseClass1", azrtti_typeid<IAnimTrack>());
+        }
+
+        return true;
+    }
+
+    template<>
+    inline void TAnimTrack<IStringKey>::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<TAnimTrack<IStringKey>, IAnimTrack>()
+                ->Version(3, &StringTrackVersionConverter)
+                ->Field("Flags", &TAnimTrack<IStringKey>::m_flags)
+                ->Field("Range", &TAnimTrack<IStringKey>::m_timeRange)
+                ->Field("ParamType", &TAnimTrack<IStringKey>::m_nParamType)
+                ->Field("Keys", &TAnimTrack<IStringKey>::m_keys)
+                ->Field("Id", &TAnimTrack<IStringKey>::m_id);
+        }
+    }
+
+    void CStringTrack::Reflect(AZ::ReflectContext* context)
+    {
+        TAnimTrack<IStringKey>::Reflect(context);
+
+        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<CStringTrack, TAnimTrack<IStringKey>>()->Version(1)->Field(
+                "DefaultValue", &CStringTrack::m_defaultValue);
+            ;
+        }
+    }
+} // namespace Maestro

+ 46 - 0
Gems/Maestro/Code/Source/Cinematics/StringTrack.h

@@ -0,0 +1,46 @@
+/*
+ * 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 <IMovieSystem.h>
+#include "AnimTrack.h"
+
+namespace Maestro
+{
+    /** String track, a key on this track has a string value.
+     */
+    class CStringTrack : public TAnimTrack<IStringKey>
+    {
+    public:
+        AZ_CLASS_ALLOCATOR(CStringTrack, AZ::SystemAllocator);
+        AZ_RTTI(CStringTrack, "{FEF911E3-30A4-4D22-BFFB-8EF4FB7CD4DB}", IAnimTrack);
+
+        // IAnimTrack -> TAnimTrack<IStringKey> overrides
+        AnimValueType GetValueType() override;
+
+        int CreateKey(float time) override; // override to use default value
+        void GetValue(float time, AZStd::string& value) override;
+        void SetValue(float time, const AZStd::string& value, bool bDefault = false) override;
+        void SerializeKey([[maybe_unused]] IStringKey& key, [[maybe_unused]] XmlNodeRef& keyNode, [[maybe_unused]] bool bLoading) override {}
+        void GetKeyInfo(int key, const char*& description, float& duration) override;
+        // ~ IAnimTrack overrides
+
+        void SetDefaultValue(const AZStd::string& defaultValue) { m_defaultValue = defaultValue; }
+        const AZStd::string& GetDefaultValue() const { return m_defaultValue; }
+
+        static void Reflect(AZ::ReflectContext* context);
+
+    private:
+        void SetKeyAtTime(float time, IKey* key);
+
+        AZStd::string m_defaultValue;
+    };
+
+} // namespace Maestro
+

+ 13 - 0
Gems/Maestro/Code/Source/Components/SequenceAgent.cpp

@@ -202,6 +202,13 @@ namespace Maestro
                 findIter->second->m_setter->m_event->Invoke(entityId, assetIdValue);
                 changed = true;
             }
+            else if (propertyTypeId == AZ::AzTypeInfo<AZStd::string>::Uuid())
+            {
+                AZStd::string stringValue;
+                value.GetValue(stringValue);
+                findIter->second->m_setter->m_event->Invoke(entityId, stringValue);
+                changed = true;
+            }
             else
             {
                 // fall-through default is to cast to float
@@ -265,6 +272,12 @@ namespace Maestro
                 findIter->second->m_getter->m_event->InvokeResult(assetIdValue, entityId);
                 returnValue.SetValue(assetIdValue);
             }
+            else if (propertyTypeId == AZ::AzTypeInfo<AZStd::string>::Uuid())
+            {
+                AZStd::string stringValue;
+                findIter->second->m_getter->m_event->InvokeResult(stringValue, entityId);
+                returnValue.SetValue(stringValue);
+            }
             else
             {
                 // fall-through default is to cast to float

+ 2 - 0
Gems/Maestro/Code/Source/Components/SequenceComponent.cpp

@@ -31,6 +31,7 @@
 #include <Cinematics/SelectTrack.h>
 #include <Cinematics/SequenceTrack.h>
 #include <Cinematics/SoundTrack.h>
+#include <Cinematics/StringTrack.h>
 #include <Cinematics/TimeRangesTrack.h>
 #include <Cinematics/TrackEventTrack.h>
 
@@ -167,6 +168,7 @@ namespace Maestro
         CSelectTrack::Reflect(context);
         CSequenceTrack::Reflect(context);
         CSoundTrack::Reflect(context);
+        CStringTrack::Reflect(context);
         CTrackEventTrack::Reflect(context);
         CAssetBlendTrack::Reflect(context);
         CTimeRangesTrack::Reflect(context);

+ 2 - 0
Gems/Maestro/Code/maestro_static_files.cmake

@@ -37,6 +37,7 @@ set(FILES
     Source/Cinematics/SelectTrack.cpp
     Source/Cinematics/SequenceTrack.cpp
     Source/Cinematics/SoundTrack.cpp
+    Source/Cinematics/StringTrack.cpp
     Source/Cinematics/TrackEventTrack.cpp
     Source/Cinematics/AnimSplineTrack.h
     Source/Cinematics/AnimSplineTrack_Vec2Specialization.cpp
@@ -55,6 +56,7 @@ set(FILES
     Source/Cinematics/SelectTrack.h
     Source/Cinematics/SequenceTrack.h
     Source/Cinematics/SoundTrack.h
+    Source/Cinematics/StringTrack.h
     Source/Cinematics/TrackEventTrack.h
     Source/Cinematics/AnimAZEntityNode.cpp
     Source/Cinematics/AnimNode.cpp