孟宇 il y a 3 mois
Parent
commit
888078709e
42 fichiers modifiés avec 9556 ajouts et 213 suppressions
  1. 18 1
      Ability/Plugins/EzAbility/Source/EzAbility/EzAbility.Build.cs
  2. 311 0
      Ability/Plugins/EzAbility/Source/EzAbility/Private/Debugger/EzAbilityDebuggerTypes.cpp
  3. 382 0
      Ability/Plugins/EzAbility/Source/EzAbility/Private/Debugger/EzAbilityTraceTypes.cpp
  4. 110 11
      Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityContext.cpp
  5. 24 0
      Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityDelegates.cpp
  6. 1862 0
      Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityPropertyBindings.cpp
  7. 1 1
      Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityPropertyRef.cpp
  8. 279 0
      Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityPropertyRefHelpers.cpp
  9. 11 0
      Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilitySchema.cpp
  10. 16 1
      Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityTypes.cpp
  11. 29 0
      Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbility_Replicate.cpp
  12. 311 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/Debugger/EzAbilityDebuggerTypes.h
  13. 257 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/Debugger/EzAbilityTraceTypes.h
  14. 28 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbility.h
  15. 15 2
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityContext.h
  16. 74 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityDelegates.h
  17. 105 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityExecutionTypes.h
  18. 30 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityNodeBase.h
  19. 1069 5
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityPropertyBindings.h
  20. 186 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityPropertyRef.h
  21. 171 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityPropertyRefHelpers.h
  22. 52 0
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilitySchema.h
  23. 367 23
      Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityTypes.h
  24. 0 17
      Ability/Plugins/EzAbility/Source/EzAbility/Public/Transition/EzAbilityTransition.h
  25. 42 12
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/EzAbilityEditor.Build.cs
  26. 2 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditor.cpp
  27. 1128 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditorData.cpp
  28. 4 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditorNode.cpp
  29. 204 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditorPropertyBindings.cpp
  30. 58 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditorTypes.cpp
  31. 258 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityPropertyHelpers.cpp
  32. 522 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityState.cpp
  33. 2 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditor.h
  34. 284 3
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditorData.h
  35. 79 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditorNode.h
  36. 124 123
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditorPropertyBindings.h
  37. 95 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditorTypes.h
  38. 198 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityPropertyHelpers.h
  39. 274 0
      Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityState.h
  40. 46 6
      Ability/Plugins/EzAbility/Source/EzAbilityTest/Private/EzAbilityTest.cpp
  41. 7 8
      Ability/Plugins/EzAbility/Source/EzAbilityTest/Private/EzAbilityTest.h
  42. 521 0
      Ability/Plugins/EzAbility/Source/EzAbilityTest/Private/EzAblityTestTypes.h

+ 18 - 1
Ability/Plugins/EzAbility/Source/EzAbility/EzAbility.Build.cs

@@ -28,8 +28,12 @@ public class EzAbility : ModuleRules
 				"Core",
 				"StructUtils",
 				"DeveloperSettings",
+				"Engine",
+				"AIModule",
 				"NetCore",
-				"GameplayTags"
+				"GameplayTags",
+				"StructUtils",
+				"StructUtilsEngine",
 			}
 		);
 			
@@ -41,6 +45,7 @@ public class EzAbility : ModuleRules
 				"Engine",
 				"Slate",
 				"SlateCore",
+				"PropertyPath",
 			}
 		);
 		
@@ -52,6 +57,18 @@ public class EzAbility : ModuleRules
 			}
 		);
 		
+		UnsafeTypeCastWarningLevel = WarningLevel.Error;
+		
+		if (Target.bBuildEditor)
+		{
+			PublicDependencyModuleNames.AddRange(
+				new string[] {
+					"UnrealEd",
+					"BlueprintGraph",
+				}
+			);
+		}
+		
 		if (Target.Platform == UnrealTargetPlatform.Win64 && 
 		    (Target.Configuration != UnrealTargetConfiguration.Shipping || Target.bBuildEditor))
 		{

+ 311 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Private/Debugger/EzAbilityDebuggerTypes.cpp

@@ -0,0 +1,311 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#if WITH_EZABILITY_DEBUGGER
+
+#include "Debugger/EzAbilityDebuggerTypes.h"
+
+namespace UE::EzAbilityDebugger
+{
+
+//----------------------------------------------------------------//
+// FInstanceDescriptor
+//----------------------------------------------------------------//
+FInstanceDescriptor::FInstanceDescriptor(const UEzAbility* InEzAbility, const FEzAbilityInstanceDebugId InId, const FString& InName, const TRange<double> InLifetime)
+	: Lifetime(InLifetime)
+	, EzAbility(InEzAbility)
+	, Name(InName)
+	, Id(InId)
+{
+}
+
+bool FInstanceDescriptor::IsValid() const
+{
+	return EzAbility.IsValid() && Name.Len() && Id.IsValid();
+}
+
+
+//----------------------------------------------------------------//
+// FInstanceEventCollection
+//----------------------------------------------------------------//
+const FInstanceEventCollection FInstanceEventCollection::Invalid;
+
+
+//----------------------------------------------------------------//
+// FScrubState
+//----------------------------------------------------------------//
+bool FScrubState::SetScrubTime(const double NewScrubTime)
+{
+	if (NewScrubTime == ScrubTime)
+	{
+		return false;
+	}
+
+	ScrubTimeBoundState = EScrubTimeBoundState::Unset;
+	TraceFrameIndex = INDEX_NONE;
+	FrameSpanIndex = INDEX_NONE;
+	ActiveStatesIndex = INDEX_NONE;
+
+	if (EventCollectionIndex != INDEX_NONE)
+	{
+		const TArray<FFrameSpan>& Spans = EventCollections[EventCollectionIndex].FrameSpans;
+		if (Spans.Num() > 0)
+		{
+			const double SpansLowerBound = Spans[0].GetWorldTimeStart();
+			const double SpansUpperBound =  Spans.Last().GetWorldTimeEnd();
+			
+			if (NewScrubTime < SpansLowerBound)
+			{
+				ScrubTimeBoundState = EScrubTimeBoundState::BeforeLowerBound;
+			}
+			else if (NewScrubTime > SpansUpperBound)
+			{
+				ScrubTimeBoundState = EScrubTimeBoundState::AfterHigherBound;
+				UpdateActiveStatesIndex(Spans.Num() - 1);
+			}
+			else
+			{
+				const uint32 NextFrameSpanIndex = Spans.IndexOfByPredicate([NewScrubTime](const FFrameSpan& Span)
+					{
+						return Span.GetWorldTimeStart() > NewScrubTime;
+					});
+
+				
+				ensure(NextFrameSpanIndex == INDEX_NONE || NextFrameSpanIndex > 0);
+				SetFrameSpanIndex(NextFrameSpanIndex != INDEX_NONE ? NextFrameSpanIndex-1 : Spans.Num() - 1);
+			}
+		}
+	}
+
+	// This will set back to the exact value provided since SetFrameSpanIndex will snap it to the start time of the matching frame.
+	// It will be consistent with the case where EventCollectionIndex is not set.
+	ScrubTime = NewScrubTime;
+
+	return true;
+}
+
+void FScrubState::SetEventCollectionIndex(const int32 InEventCollectionIndex)
+{
+	EventCollectionIndex = InEventCollectionIndex;
+
+	// Force refresh of internal indices with current time applied on the new event collection. 
+	const double PrevScrubTime = ScrubTime;
+	ScrubTime = 0;
+	SetScrubTime(PrevScrubTime);
+}
+
+void FScrubState::SetFrameSpanIndex(const int32 NewFrameSpanIndex)
+{
+	FrameSpanIndex = NewFrameSpanIndex;
+	checkf(EventCollections.IsValidIndex(EventCollectionIndex), TEXT("Internal method expecting validity checks before getting called."));
+	const FInstanceEventCollection& EventCollection = EventCollections[EventCollectionIndex];
+
+	checkf(EventCollections[EventCollectionIndex].FrameSpans.IsValidIndex(FrameSpanIndex), TEXT("Internal method expecting validity checks before getting called."));
+	ScrubTime = EventCollection.FrameSpans[FrameSpanIndex].GetWorldTimeStart();
+	TraceFrameIndex = EventCollection.FrameSpans[FrameSpanIndex].Frame.Index;
+	ScrubTimeBoundState = EScrubTimeBoundState::InBounds;
+	UpdateActiveStatesIndex(NewFrameSpanIndex);
+}
+
+void FScrubState::SetActiveStatesIndex(const int32 NewActiveStatesIndex)
+{
+	ActiveStatesIndex = NewActiveStatesIndex;
+
+	checkf(EventCollections.IsValidIndex(EventCollectionIndex), TEXT("Internal method expecting validity checks before getting called."));
+	const FInstanceEventCollection& EventCollection = EventCollections[EventCollectionIndex];
+
+	checkf(EventCollection.ActiveStatesChanges.IsValidIndex(ActiveStatesIndex), TEXT("Internal method expecting validity checks before getting called."));
+	FrameSpanIndex = EventCollection.ActiveStatesChanges[ActiveStatesIndex].SpanIndex;
+	ScrubTime = EventCollection.FrameSpans[FrameSpanIndex].GetWorldTimeStart();
+	TraceFrameIndex = EventCollection.FrameSpans[FrameSpanIndex].Frame.Index;
+	ScrubTimeBoundState = EScrubTimeBoundState::InBounds;
+}
+
+bool FScrubState::HasPreviousFrame() const
+{
+	if (EventCollectionIndex != INDEX_NONE)
+	{
+		return IsInBounds() ? EventCollections[EventCollectionIndex].FrameSpans.IsValidIndex(FrameSpanIndex - 1) : ScrubTimeBoundState == EScrubTimeBoundState::AfterHigherBound;
+	}
+	return false;
+}
+
+double FScrubState::GotoPreviousFrame()
+{
+	SetFrameSpanIndex(IsInBounds() ? (FrameSpanIndex - 1) : EventCollections[EventCollectionIndex].FrameSpans.Num()-1);
+	return ScrubTime;
+}
+
+bool FScrubState::HasNextFrame() const
+{
+	if (EventCollectionIndex != INDEX_NONE)
+	{
+		return IsInBounds()	? EventCollections[EventCollectionIndex].FrameSpans.IsValidIndex(FrameSpanIndex + 1) : ScrubTimeBoundState == EScrubTimeBoundState::BeforeLowerBound;
+	}
+	return false;
+}
+
+double FScrubState::GotoNextFrame()
+{
+	SetFrameSpanIndex(IsInBounds() ? (FrameSpanIndex + 1) : 0);
+	return ScrubTime;
+}
+
+bool FScrubState::HasPreviousActiveStates() const
+{
+	if (EventCollectionIndex == INDEX_NONE || ActiveStatesIndex == INDEX_NONE)
+	{
+		return false;
+	}
+
+	const TArray<FInstanceEventCollection::FActiveStatesChangePair>& ActiveStatesChanges = EventCollections[EventCollectionIndex].ActiveStatesChanges;
+	if (ScrubTimeBoundState == EScrubTimeBoundState::AfterHigherBound && ActiveStatesChanges.Num() > 0)
+	{
+		return true;
+	}
+
+	if (ActiveStatesChanges.IsValidIndex(ActiveStatesIndex) && ActiveStatesChanges[ActiveStatesIndex].SpanIndex < FrameSpanIndex)
+	{
+		return true;
+	}
+
+	return ActiveStatesChanges.IsValidIndex(ActiveStatesIndex - 1);
+}
+
+double FScrubState::GotoPreviousActiveStates()
+{
+	const TArray<FInstanceEventCollection::FActiveStatesChangePair>& ActiveStatesChanges = EventCollections[EventCollectionIndex].ActiveStatesChanges;
+	if (ScrubTimeBoundState == EScrubTimeBoundState::AfterHigherBound)
+	{
+		SetActiveStatesIndex(ActiveStatesChanges.Num()-1);
+	}
+	else if (ActiveStatesChanges.IsValidIndex(ActiveStatesIndex) && ActiveStatesChanges[ActiveStatesIndex].SpanIndex < FrameSpanIndex)
+	{
+		SetActiveStatesIndex(ActiveStatesIndex);
+	}
+	else
+	{
+		SetActiveStatesIndex(ActiveStatesIndex - 1);
+	}
+
+	return ScrubTime;
+}
+
+bool FScrubState::HasNextActiveStates() const
+{
+	if (EventCollectionIndex == INDEX_NONE)
+	{
+		return false;
+	}
+
+	const TArray<FInstanceEventCollection::FActiveStatesChangePair>& ActiveStatesChanges = EventCollections[EventCollectionIndex].ActiveStatesChanges;
+	if (ScrubTimeBoundState == EScrubTimeBoundState::BeforeLowerBound && ActiveStatesChanges.Num() > 0)
+	{
+		return true;
+	}
+
+	return ActiveStatesIndex != INDEX_NONE && EventCollections[EventCollectionIndex].ActiveStatesChanges.IsValidIndex(ActiveStatesIndex + 1);
+}
+
+double FScrubState::GotoNextActiveStates()
+{
+	if (ScrubTimeBoundState == EScrubTimeBoundState::BeforeLowerBound)
+	{
+		SetActiveStatesIndex(0);
+	}
+	else
+	{
+		SetActiveStatesIndex(ActiveStatesIndex + 1);
+	}
+	return ScrubTime;
+}
+
+const FInstanceEventCollection& FScrubState::GetEventCollection() const
+{
+	return EventCollectionIndex != INDEX_NONE ? EventCollections[EventCollectionIndex] : FInstanceEventCollection::Invalid;
+}
+
+void FScrubState::UpdateActiveStatesIndex(const int32 SpanIndex)
+{
+	check(EventCollectionIndex != INDEX_NONE);
+	const FInstanceEventCollection& EventCollection = EventCollections[EventCollectionIndex];
+
+	// Need to find the index of a frame span that contains an active states changed event; either the current one has it otherwise look backward to find the last one
+	ActiveStatesIndex = EventCollection.ActiveStatesChanges.FindLastByPredicate(
+		[SpanIndex](const FInstanceEventCollection::FActiveStatesChangePair& SpanAndEventIndices)
+		{
+			return SpanAndEventIndices.SpanIndex <= SpanIndex;
+		});
+}
+} // UE::EzAbilityDebugger
+
+FEzAbilityDebuggerBreakpoint::FEzAbilityDebuggerBreakpoint()
+	: BreakpointType(EEzAbilityBreakpointType::Unset)
+	, EventType(EEzAbilityTraceEventType::Unset)
+{
+}
+
+FEzAbilityDebuggerBreakpoint::FEzAbilityDebuggerBreakpoint(const FEzAbilityStateHandle StateHandle, const EEzAbilityBreakpointType BreakpointType)
+	: ElementIdentifier(TInPlaceType<FEzAbilityStateHandle>(), StateHandle)
+	, BreakpointType(BreakpointType)
+{
+	EventType = GetMatchingEventType(BreakpointType);
+}
+
+FEzAbilityDebuggerBreakpoint::FEzAbilityDebuggerBreakpoint(const FEzAbilityTaskIndex Index, const EEzAbilityBreakpointType BreakpointType)
+	: ElementIdentifier(TInPlaceType<FEzAbilityTaskIndex>(), Index)
+	, BreakpointType(BreakpointType)
+{
+	EventType = GetMatchingEventType(BreakpointType);
+}
+
+FEzAbilityDebuggerBreakpoint::FEzAbilityDebuggerBreakpoint(const FEzAbilityTransitionIndex Index, const EEzAbilityBreakpointType BreakpointType)
+	: ElementIdentifier(TInPlaceType<FEzAbilityTransitionIndex>(), Index)
+	, BreakpointType(BreakpointType)
+{
+	EventType = GetMatchingEventType(BreakpointType);
+}
+
+bool FEzAbilityDebuggerBreakpoint::IsMatchingEvent(const FEzAbilityTraceEventVariantType& Event) const
+{
+	EEzAbilityTraceEventType ReceivedEventType = EEzAbilityTraceEventType::Unset;
+	Visit([&ReceivedEventType](auto& TypedEvent) { ReceivedEventType = TypedEvent.EventType; }, Event);
+
+	bool bIsMatching = false;
+	if (EventType == ReceivedEventType)
+	{
+		if (const FEzAbilityStateHandle* StateHandle = ElementIdentifier.TryGet<FEzAbilityStateHandle>())
+		{
+			const FEzAbilityTraceStateEvent* StateEvent = Event.TryGet<FEzAbilityTraceStateEvent>();
+			bIsMatching = StateEvent != nullptr && StateEvent->GetStateHandle() == *StateHandle;
+		}
+		else if (const FEzAbilityTaskIndex* TaskIndex = ElementIdentifier.TryGet<FEzAbilityTaskIndex>())
+		{
+			const FEzAbilityTraceTaskEvent* TaskEvent = Event.TryGet<FEzAbilityTraceTaskEvent>();
+			bIsMatching = TaskEvent != nullptr && TaskEvent->Index == TaskIndex->Index;
+		}
+		else if (const FEzAbilityTransitionIndex* TransitionIndex = ElementIdentifier.TryGet<FEzAbilityTransitionIndex>())
+		{
+			const FEzAbilityTraceTransitionEvent* TransitionEvent = Event.TryGet<FEzAbilityTraceTransitionEvent>();
+			bIsMatching = TransitionEvent != nullptr && TransitionEvent->TransitionSource.TransitionIndex == TransitionIndex->Index;
+		}
+	}
+	return bIsMatching;
+}
+
+EEzAbilityTraceEventType FEzAbilityDebuggerBreakpoint::GetMatchingEventType(const EEzAbilityBreakpointType BreakpointType)
+{
+	switch (BreakpointType)
+	{
+	case EEzAbilityBreakpointType::OnEnter:
+		return EEzAbilityTraceEventType::OnEntered;
+	case EEzAbilityBreakpointType::OnExit:
+		return EEzAbilityTraceEventType::OnExited;
+	case EEzAbilityBreakpointType::OnTransition:
+		return EEzAbilityTraceEventType::OnTransition;
+	default:
+		return EEzAbilityTraceEventType::Unset;
+	}
+}
+
+#endif // WITH_EZABILITY_DEBUGGER
+

+ 382 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Private/Debugger/EzAbilityTraceTypes.cpp

@@ -0,0 +1,382 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "Debugger/EzAbilityTraceTypes.h"
+
+#include "EzAbility.h"
+#include "EzAbilityNodeBase.h"
+
+#if WITH_EZABILITY_DEBUGGER
+
+namespace UE::EzAbilityTrace
+{
+	FString GetStateName(const UEzAbility& EzAbility, const FCompactEzAbilityState* CompactState)
+	{
+		check(CompactState);
+
+		if (const UEzAbility* LinkedAsset = CompactState->LinkedAsset.Get())
+		{
+			return FString::Printf(TEXT("%s > [%s]"), *CompactState->Name.ToString(), *LinkedAsset->GetName());
+		}
+
+		if (const FCompactEzAbilityState* LinkedState = EzAbility.GetStateFromHandle(CompactState->LinkedState))
+		{
+			return FString::Printf(TEXT("%s > %s"), *CompactState->Name.ToString(), *LinkedState->Name.ToString());
+		}
+
+		return CompactState->Name.ToString();
+	}
+}
+
+//----------------------------------------------------------------------//
+// FEzAbilityTracePhaseEvent
+//----------------------------------------------------------------------//
+FString FEzAbilityTracePhaseEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	return GetValueString(EzAbility);
+}
+
+FString FEzAbilityTracePhaseEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	FStringBuilderBase StrBuilder;
+
+	// Override to display only selection behavior
+	if (Phase == EEzAbilityUpdatePhase::TrySelectBehavior
+		&& StateHandle.IsValid())
+	{
+		if (const FCompactEzAbilityState* CompactState = EzAbility.GetStateFromHandle(StateHandle))
+		{
+			return UEnum::GetDisplayValueAsText(CompactState->SelectionBehavior).ToString();
+		}
+
+		return FString::Printf(TEXT("Invalid State Index %s for '%s'"), *StateHandle.Describe(), *EzAbility.GetFullName());
+	}
+
+	// Otherwise build either: "<PhaseDescription> '<StateName>'" or "<StateName>"
+	if (Phase != EEzAbilityUpdatePhase::Unset)
+	{
+		StrBuilder.Append(UEnum::GetDisplayValueAsText(Phase).ToString());
+	}
+
+	const FCompactEzAbilityState* CompactState = EzAbility.GetStateFromHandle(StateHandle);
+	if (CompactState != nullptr || StateHandle.IsValid())
+	{
+		if (StrBuilder.Len())
+		{
+			StrBuilder.Appendf(TEXT(" '%s'"),
+				CompactState != nullptr ? *UE::EzAbilityTrace::GetStateName(EzAbility, CompactState) : *StateHandle.Describe());
+		}
+		else
+		{
+			StrBuilder.Appendf(TEXT("%s"),
+				CompactState != nullptr ? *UE::EzAbilityTrace::GetStateName(EzAbility, CompactState) : *StateHandle.Describe());
+		}
+	}
+
+	return StrBuilder.ToString();
+}
+
+FString FEzAbilityTracePhaseEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return *UEnum::GetDisplayValueAsText(EventType).ToString();
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceLogEvent
+//----------------------------------------------------------------------//
+FString FEzAbilityTraceLogEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	return FString::Printf(TEXT("%s: %s"), *GetTypeString(EzAbility), *GetValueString(EzAbility));
+}
+
+FString FEzAbilityTraceLogEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	return (*Message);
+}
+
+FString FEzAbilityTraceLogEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return TEXT("Log");
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTracePropertyEvent
+//----------------------------------------------------------------------//
+FString FEzAbilityTracePropertyEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	return FString::Printf(TEXT("%s: %s"), *GetTypeString(EzAbility), *GetValueString(EzAbility));
+}
+
+FString FEzAbilityTracePropertyEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	return (*Message);
+}
+
+FString FEzAbilityTracePropertyEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return TEXT("Property value");
+}
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceTransitionEvent
+//----------------------------------------------------------------------//
+FString FEzAbilityTraceTransitionEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	return FString::Printf(TEXT("%s %s"), *GetTypeString(EzAbility), *GetValueString(EzAbility));
+}
+
+FString FEzAbilityTraceTransitionEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	const FCompactEzAbilityState* CompactState = EzAbility.GetStateFromHandle(TransitionSource.TargetState);
+	FStringBuilderBase StrBuilder;
+	StrBuilder.Appendf(TEXT("go to State '%s'"), CompactState != nullptr ? *UE::EzAbilityTrace::GetStateName(EzAbility, CompactState) : *TransitionSource.TargetState.Describe());
+
+	if (TransitionSource.Priority != EEzAbilityTransitionPriority::None)
+	{
+		StrBuilder.Appendf(TEXT(" (Priority: %s)"), *UEnum::GetDisplayValueAsText(TransitionSource.Priority).ToString()); 
+	}
+
+	if (TransitionSource.SourceType == EEzAbilityTransitionSourceType::Asset)
+	{
+		if (const FCompactEzAbilityTransition* Transition = EzAbility.GetTransitionFromIndex(TransitionSource.TransitionIndex))
+		{
+			ensureAlways(Transition->Priority == TransitionSource.Priority);
+			ensureAlways(Transition->State == TransitionSource.TargetState);
+			if (Transition->EventTag.IsValid())
+			{
+				StrBuilder.Appendf(TEXT("\n\t%s"), *Transition->EventTag.ToString()); 
+			}
+		}
+		else
+		{
+			StrBuilder.Appendf(TEXT("Invalid Transition Index %s for '%s'"), *LexToString(TransitionSource.TransitionIndex.Get()), *EzAbility.GetFullName());
+		}
+	}
+
+	return StrBuilder.ToString();
+}
+
+FString FEzAbilityTraceTransitionEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	if (TransitionSource.SourceType == EEzAbilityTransitionSourceType::Asset)
+	{
+		if (const FCompactEzAbilityTransition* Transition = EzAbility.GetTransitionFromIndex(TransitionSource.TransitionIndex))
+		{
+			return *UEnum::GetDisplayValueAsText(Transition->Trigger).ToString();
+		}
+
+		return FString::Printf(TEXT("Invalid Transition Index %s for '%s'"), *LexToString(TransitionSource.TransitionIndex.Get()), *EzAbility.GetFullName());
+	}
+
+	return *UEnum::GetDisplayValueAsText(TransitionSource.SourceType).ToString();
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceNodeEvent
+//----------------------------------------------------------------------//
+FString FEzAbilityTraceNodeEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	const FConstStructView NodeView = Index.IsValid() ? EzAbility.GetNode(Index.Get()) : FConstStructView();
+	const FEzAbilityNodeBase* Node = NodeView.GetPtr<const FEzAbilityNodeBase>();
+
+	return FString::Printf(TEXT("%s '%s (%s)'"),
+			*UEnum::GetDisplayValueAsText(EventType).ToString(),
+			Node != nullptr ? *Node->Name.ToString() : *LexToString(Index.Get()),
+			NodeView.IsValid() ? *NodeView.GetScriptStruct()->GetName() : TEXT("Invalid Node"));
+}
+
+FString FEzAbilityTraceNodeEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	const FConstStructView NodeView = Index.IsValid() ? EzAbility.GetNode(Index.Get()) : FConstStructView();
+	const FEzAbilityNodeBase* Node = NodeView.GetPtr<const FEzAbilityNodeBase>();
+
+	return FString::Printf(TEXT("%s '%s'"),
+			*UEnum::GetDisplayValueAsText(EventType).ToString(),
+			Node != nullptr ? *Node->Name.ToString() : *LexToString(Index.Get()));
+}
+
+FString FEzAbilityTraceNodeEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	const FConstStructView NodeView = Index.IsValid() ? EzAbility.GetNode(Index.Get()) : FConstStructView();
+	return FString::Printf(TEXT("%s"), NodeView.IsValid() ? *NodeView.GetScriptStruct()->GetName() : TEXT("Invalid Node"));
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceStateEvent
+//----------------------------------------------------------------------//
+FEzAbilityStateHandle FEzAbilityTraceStateEvent::GetStateHandle() const
+{
+	return FEzAbilityStateHandle(Index.Get());
+}
+
+FString FEzAbilityTraceStateEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	const FEzAbilityStateHandle StateHandle(Index.Get());
+	if (const FCompactEzAbilityState* CompactState = EzAbility.GetStateFromHandle(StateHandle))
+	{
+		if (CompactState->SelectionBehavior != EEzAbilityStateSelectionBehavior::None)
+		{
+			return FString::Printf(TEXT("%s '%s' (%s)"),
+				*UEnum::GetDisplayValueAsText(EventType).ToString(),
+				*GetValueString(EzAbility),
+				*UEnum::GetDisplayValueAsText(CompactState->SelectionBehavior).ToString());
+		}
+	}
+
+	return FString::Printf(TEXT("%s '%s'"),
+		*UEnum::GetDisplayValueAsText(EventType).ToString(),
+		*GetValueString(EzAbility));
+}
+
+FString FEzAbilityTraceStateEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	const FEzAbilityStateHandle StateHandle(Index.Get());
+	if (const FCompactEzAbilityState* CompactState = EzAbility.GetStateFromHandle(StateHandle))
+	{
+		return UE::EzAbilityTrace::GetStateName(EzAbility, CompactState);
+	}
+
+	return FString::Printf(TEXT("Invalid State Index %s for '%s'"), *StateHandle.Describe(), *EzAbility.GetFullName());
+}
+
+FString FEzAbilityTraceStateEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return TEXT("State");
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceTaskEvent
+//----------------------------------------------------------------------//
+FString FEzAbilityTraceTaskEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	return FString::Printf(TEXT("%s -> %s"), *FEzAbilityTraceNodeEvent::ToFullString(EzAbility), *UEnum::GetDisplayValueAsText(Status).ToString());
+}
+
+FString FEzAbilityTraceTaskEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	const FConstStructView NodeView = Index.IsValid() ? EzAbility.GetNode(Index.Get()) : FConstStructView();
+	const FEzAbilityNodeBase* Node = NodeView.GetPtr<const FEzAbilityNodeBase>();
+	return FString::Printf(TEXT("%s"), Node != nullptr ? *Node->Name.ToString() : *LexToString(Index.Get()));
+}
+
+FString FEzAbilityTraceTaskEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return FEzAbilityTraceNodeEvent::GetTypeString(EzAbility);
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceEvaluatorEvent
+//----------------------------------------------------------------------//
+FString FEzAbilityTraceEvaluatorEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	return FEzAbilityTraceNodeEvent::ToFullString(EzAbility);
+}
+
+FString FEzAbilityTraceEvaluatorEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	const FConstStructView NodeView = Index.IsValid() ? EzAbility.GetNode(Index.Get()) : FConstStructView();
+	const FEzAbilityNodeBase* Node = NodeView.GetPtr<const FEzAbilityNodeBase>();
+	return FString::Printf(TEXT("%s"), Node != nullptr ? *Node->Name.ToString() : *LexToString(Index.Get()));
+}
+
+FString FEzAbilityTraceEvaluatorEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return FEzAbilityTraceNodeEvent::GetTypeString(EzAbility);
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceConditionEvent
+//----------------------------------------------------------------------//
+FString FEzAbilityTraceConditionEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	return FEzAbilityTraceNodeEvent::ToFullString(EzAbility);
+}
+
+FString FEzAbilityTraceConditionEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	const FConstStructView NodeView = Index.IsValid() ? EzAbility.GetNode(Index.Get()) : FConstStructView();
+	const FEzAbilityNodeBase* Node = NodeView.GetPtr<const FEzAbilityNodeBase>();
+
+	return FString::Printf(TEXT("%s"), Node != nullptr ? *Node->Name.ToString() : *LexToString(Index.Get()));
+}
+
+FString FEzAbilityTraceConditionEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return FEzAbilityTraceNodeEvent::GetTypeString(EzAbility);
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceActiveStatesEvent
+//----------------------------------------------------------------------//
+FEzAbilityTraceActiveStatesEvent::FEzAbilityTraceActiveStatesEvent(const double RecordingWorldTime)
+	: FEzAbilityTraceBaseEvent(RecordingWorldTime, EEzAbilityTraceEventType::Unset)
+{
+}
+
+FString FEzAbilityTraceActiveStatesEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	if (ActiveStates.PerAssetStates.Num() > 0)
+	{
+		return FString::Printf(TEXT("%s: %s"), *GetTypeString(EzAbility), *GetValueString(EzAbility));
+	}
+	return TEXT("No active states");
+}
+
+FString FEzAbilityTraceActiveStatesEvent::GetValueString(const UEzAbility&) const
+{
+	FStringBuilderBase StatePath;
+	for (const FEzAbilityTraceActiveStates::FAssetActiveStates& PerAssetStates : ActiveStates.PerAssetStates)
+	{
+		if (const UEzAbility* EzAbility = PerAssetStates.WeakEzAbility.Get())
+		{
+			for (const FEzAbilityStateHandle Handle : PerAssetStates.ActiveStates)
+			{
+				const FCompactEzAbilityState* State = EzAbility->GetStateFromHandle(Handle);
+				StatePath.Appendf(TEXT("%s%s"),
+					StatePath.Len() == 0 ? TEXT("") : TEXT("."),
+					State == nullptr ? TEXT("Invalid") : *UE::EzAbilityTrace::GetStateName(*EzAbility, State));
+			}
+		}
+	}
+	return StatePath.ToString();
+}
+
+FString FEzAbilityTraceActiveStatesEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return TEXT("New active states");
+}
+
+
+//----------------------------------------------------------------------//
+// FEzAbilityTraceInstanceFrameEvent
+//----------------------------------------------------------------------//
+FEzAbilityTraceInstanceFrameEvent::FEzAbilityTraceInstanceFrameEvent(const double RecordingWorldTime, const EEzAbilityTraceEventType EventType, const UEzAbility* EzAbility)
+	: FEzAbilityTraceBaseEvent(RecordingWorldTime, EventType)
+	, WeakEzAbility(EzAbility)
+{
+}
+
+FString FEzAbilityTraceInstanceFrameEvent::ToFullString(const UEzAbility& EzAbility) const
+{
+	return FString::Printf(TEXT("%s: %s"), *GetTypeString(EzAbility), *GetValueString(EzAbility));
+}
+
+FString FEzAbilityTraceInstanceFrameEvent::GetValueString(const UEzAbility& EzAbility) const
+{
+	return GetNameSafe(WeakEzAbility.Get());
+}
+
+FString FEzAbilityTraceInstanceFrameEvent::GetTypeString(const UEzAbility& EzAbility) const
+{
+	return TEXT("New instance frame");
+}
+
+#endif // WITH_EZABILITY_DEBUGGER

+ 110 - 11
Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityContext.cpp

@@ -10,6 +10,7 @@
 #include "Task/EzAbilityTask.h"
 #include "EzAbilityLog.h"
 #include "Debugger/EzAbilityTrace.h"
+#include "Debugger/EzAbilityTraceTypes.h"
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 ///define
@@ -63,9 +64,18 @@
 	#define EZ_ABILITY_TRACE_TRANSITION_EVENT(Source, EventType)
 #endif // WITH_EZABILITY_DEBUGGER
 
-
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 ///Ability Context
+
+FEzAbilityContext::FEzAbilityContext(UObject& InOwner, const UEzAbility& InAbility,
+	FEzAbilityInstanceData& InInstanceData, const FOnCollectEzAbilityExternalData& CollectExternalDataCallback)
+{
+}
+
+FEzAbilityContext::~FEzAbilityContext()
+{
+}
+
 void FEzAbilityContext::InitContext(UEzAbilityComponent* Component)
 {
 	AbilityComponent = Component;
@@ -192,21 +202,110 @@ EAbilityRunStatus FEzAbilityContext::Start(UEzAbility* InAbility, const FEzAbili
 	return EAbilityRunStatus::Succeeded;
 }
 
-// EAbilityRunStatus FEzAbilityContext::Tick(float DeltaTime)
-// {
-// 	return EAbilityRunStatus::Failed;
-// }
-//
-// EAbilityRunStatus FEzAbilityContext::Stop()
-// {
-// 	return EAbilityRunStatus::Failed;
-// }
-
 EAbilityRunStatus FEzAbilityContext::GetAbilityRunStatus() const
 {
 	return EAbilityRunStatus::Failed;
 }
 
+EAbilityRunStatus FEzAbilityContext::Tick(float DeltaTime)
+{
+	return EAbilityRunStatus::Failed;
+}
+
+FEzAbilityDataView FEzAbilityContext::GetDataView(FEzAbilityInstanceStorage& InstanceDataStorage,
+                                                  FEzAbilityInstanceStorage* CurrentlyProcessedSharedInstanceStorage, const FEzAbilityExecutionFrame* ParentFrame,
+                                                  const FEzAbilityExecutionFrame& CurrentFrame, TConstArrayView<FEzAbilityDataView> ContextAndExternalDataViews,
+                                                  const FEzAbilityDataHandle Handle)
+{
+	switch (Handle.GetSource())
+	{
+	case EEzAbilityDataSourceType::None:
+		return {};
+
+	case EEzAbilityDataSourceType::GlobalInstanceData:
+		return InstanceDataStorage.GetMutableStruct(CurrentFrame.GlobalInstanceIndexBase.Get() + Handle.GetIndex());
+	case EEzAbilityDataSourceType::GlobalInstanceDataObject:
+		return InstanceDataStorage.GetMutableObject(CurrentFrame.GlobalInstanceIndexBase.Get() + Handle.GetIndex());
+		
+	case EEzAbilityDataSourceType::ActiveInstanceData:
+		return InstanceDataStorage.GetMutableStruct(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex());
+	case EEzAbilityDataSourceType::ActiveInstanceDataObject:
+		return InstanceDataStorage.GetMutableObject(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex());
+
+	case EEzAbilityDataSourceType::SharedInstanceData:
+		check(CurrentlyProcessedSharedInstanceStorage);
+		return CurrentlyProcessedSharedInstanceStorage->GetMutableStruct(Handle.GetIndex());
+	case EEzAbilityDataSourceType::SharedInstanceDataObject:
+		check(CurrentlyProcessedSharedInstanceStorage);
+		return CurrentlyProcessedSharedInstanceStorage->GetMutableObject(Handle.GetIndex());
+
+	case EEzAbilityDataSourceType::ContextData:
+		check(!ContextAndExternalDataViews.IsEmpty())
+		return ContextAndExternalDataViews[Handle.GetIndex()];
+
+	case EEzAbilityDataSourceType::ExternalData:
+		check(!ContextAndExternalDataViews.IsEmpty())
+		return ContextAndExternalDataViews[CurrentFrame.ExternalDataBaseIndex.Get() + Handle.GetIndex()];
+
+	case EEzAbilityDataSourceType::GlobalParameterData:
+		{
+			// Defined in parent frame or is root state tree parameters
+			if (ParentFrame)
+			{
+				return GetDataView(InstanceDataStorage, CurrentlyProcessedSharedInstanceStorage, nullptr, *ParentFrame, ContextAndExternalDataViews, CurrentFrame.GlobalParameterDataHandle);
+			}
+
+			return InstanceDataStorage.GetMutableGlobalParameters();
+		}
+
+	case EEzAbilityDataSourceType::SubtreeParameterData:
+		{
+			// Defined in parent frame.
+			if (ParentFrame)
+			{
+				// Linked subtree, params defined in parent scope.
+				return GetDataView(InstanceDataStorage, CurrentlyProcessedSharedInstanceStorage, nullptr, *ParentFrame, ContextAndExternalDataViews, CurrentFrame.StateParameterDataHandle);
+			}
+			// Standalone subtree, params define as state params.
+			FCompactEzAbilityParameters& Params = InstanceDataStorage.GetMutableStruct(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex()).Get<FCompactEzAbilityParameters>();
+			return Params.Parameters.GetMutableValue();
+		}
+
+	case EEzAbilityDataSourceType::StateParameterData:
+		{
+			FCompactEzAbilityParameters& Params = InstanceDataStorage.GetMutableStruct(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex()).Get<FCompactEzAbilityParameters>();
+			return Params.Parameters.GetMutableValue();
+		}
+	default:
+		checkf(false, TEXT("Unhandle case %s"), *UEnum::GetValueAsString(Handle.GetSource()));
+	}
+
+	return {};
+}
+
+TArray<FName> FEzAbilityContext::GetActiveStateNames() const
+{
+	TArray<FName> Result;	
+	const FEzAbilityExecutionState& Exec = GetExecState();
+
+	// Active States
+	for (const FEzAbilityExecutionFrame& CurrentFrame : Exec.ActiveFrames)
+	{
+		const UEzAbility* CurrentStateTree = CurrentFrame.Ability;
+		for (int32 Index = 0; Index < CurrentFrame.ActiveStates.Num(); Index++)
+		{
+			const FEzAbilityStateHandle Handle = CurrentFrame.ActiveStates[Index];
+			if (Handle.IsValid())
+			{
+				const FCompactEzAbilityState& State = CurrentStateTree->States[Handle.Index];
+				Result.Add(State.Name);
+			}
+		}
+	}
+
+	return Result;
+}
+
 void FEzAbilityContext::Reset()
 {
 	Instigator = nullptr;

+ 24 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityDelegates.cpp

@@ -0,0 +1,24 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "EzAbilityDelegates.h"
+
+namespace UE::EzAbility::Delegates
+{
+	
+#if WITH_EDITOR
+	FOnIdentifierChanged OnIdentifierChanged;
+	FOnSchemaChanged OnSchemaChanged;
+	FOnParametersChanged OnParametersChanged;
+	FOnGlobalDataChanged OnGlobalDataChanged;
+	FOnStateParametersChanged OnStateParametersChanged;
+	FOnBreakpointsChanged OnBreakpointsChanged;
+	FOnPostCompile OnPostCompile;
+	FOnRequestCompile OnRequestCompile;
+#endif // WITH_EDITOR
+
+#if WITH_EZABILITY_DEBUGGER
+	FOnTracingStateChanged OnTracingStateChanged;
+#endif // WITH_EZABILITY_DEBUGGER
+
+}; // UE::EzAbility::Delegates

+ 1862 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityPropertyBindings.cpp

@@ -2,3 +2,1865 @@
 
 
 #include "EzAbilityPropertyBindings.h"
+
+#if WITH_EDITOR
+#include "UObject/CoreRedirects.h"
+#include "UObject/Package.h"
+#include "Engine/BlueprintGeneratedClass.h"
+#include "Engine/UserDefinedStruct.h"
+#include "Kismet2/StructureEditorUtils.h"
+#include "UObject/Field.h"
+#endif
+
+#include "EzAbilityLog.h"
+#include "EzAbilityPropertyRef.h"
+#include "PropertyPathHelpers.h"
+#include "Misc/EnumerateRange.h"
+
+#include UE_INLINE_GENERATED_CPP_BY_NAME(EzAbilityPropertyBindings)
+
+namespace UE::EzAbility
+{
+	FString GetDescAndPathAsString(const FEzAbilityBindableStructDesc& Desc, const FEzAbilityPropertyPath& Path)
+	{
+		FStringBuilderBase Result;
+
+		Result += Desc.ToString();
+
+		if (!Path.IsPathEmpty())
+		{
+			Result += TEXT(" ");
+			Result += Path.ToString();
+		}
+
+		return Result.ToString();
+	}
+
+#if WITH_EDITOR
+	EEzAbilityPropertyUsage GetUsageFromMetaData(const FProperty* Property)
+	{
+		static const FName CategoryName(TEXT("Category"));
+
+		if (Property == nullptr)
+		{
+			return EEzAbilityPropertyUsage::Invalid;
+		}
+		
+		const FString Category = Property->GetMetaData(CategoryName);
+
+		if (Category == TEXT("Input"))
+		{
+			return EEzAbilityPropertyUsage::Input;
+		}
+		if (Category == TEXT("Inputs"))
+		{
+			return EEzAbilityPropertyUsage::Input;
+		}
+		if (Category == TEXT("Output"))
+		{
+			return EEzAbilityPropertyUsage::Output;
+		}
+		if (Category == TEXT("Outputs"))
+		{
+			return EEzAbilityPropertyUsage::Output;
+		}
+		if (Category == TEXT("Context"))
+		{
+			return EEzAbilityPropertyUsage::Context;
+		}
+
+		return EEzAbilityPropertyUsage::Parameter;
+	}
+#endif
+
+} // UE::EzAbility
+
+namespace UE::EzAbility::Private
+{
+
+#if WITH_EDITORONLY_DATA
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	FEzAbilityPropertyPath ConvertEditorPath(const FEzAbilityEditorPropertyPath& InEditorPath)
+	{
+		FEzAbilityPropertyPath Path;
+		Path.SetStructID(InEditorPath.StructID);
+
+		for (const FString& Segment : InEditorPath.Path)
+		{
+			const TCHAR* PropertyNamePtr = nullptr;
+			int32 PropertyNameLength = 0;
+			int32 ArrayIndex = INDEX_NONE;
+			PropertyPathHelpers::FindFieldNameAndArrayIndex(Segment.Len(), *Segment, PropertyNameLength, &PropertyNamePtr, ArrayIndex);
+			FString PropertyNameString(PropertyNameLength, PropertyNamePtr);
+			const FName PropertyName(*PropertyNameString, FNAME_Find);
+			Path.AddPathSegment(PropertyName, ArrayIndex);
+		}
+		return Path;
+	}
+
+	FEzAbilityEditorPropertyPath ConvertEditorPath(const FEzAbilityPropertyPath& InPath)
+	{
+		FEzAbilityEditorPropertyPath Path;
+		Path.StructID = InPath.GetStructID();
+		for (const FEzAbilityPropertyPathSegment& Segment : InPath.GetSegments())
+		{
+			if (Segment.GetArrayIndex() != INDEX_NONE)
+			{
+				Path.Path.Add(FString::Printf(TEXT("%s[%d]"), *Segment.GetName().ToString(), Segment.GetArrayIndex()));
+			}
+			else
+			{
+				Path.Path.Add(Segment.GetName().ToString());
+			}
+		}
+		return Path;
+	}
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+#endif // WITH_EDITORONLY_DATA
+} // UE::EzAbility::Private 
+
+
+//----------------------------------------------------------------//
+//  FEzAbilityBindableStructDesc
+//----------------------------------------------------------------//
+
+FString FEzAbilityBindableStructDesc::ToString() const
+{
+	FStringBuilderBase Result;
+
+	Result += UEnum::GetDisplayValueAsText(DataSource).ToString();
+	Result += TEXT(" '");
+	Result += Name.ToString();
+	Result += TEXT("'");
+
+	return Result.ToString();
+}
+
+//----------------------------------------------------------------//
+//  FEzAbilityPropertyPathBinding
+//----------------------------------------------------------------//
+void FEzAbilityPropertyPathBinding::PostSerialize(const FArchive& Ar)
+{
+#if WITH_EDITORONLY_DATA
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	if (SourcePath_DEPRECATED.IsValid())
+	{
+		SourcePropertyPath = UE::EzAbility::Private::ConvertEditorPath(SourcePath_DEPRECATED);
+		SourcePath_DEPRECATED.StructID = FGuid();
+		SourcePath_DEPRECATED.Path.Reset();
+	}
+
+	if (TargetPath_DEPRECATED.IsValid())
+	{
+		TargetPropertyPath = UE::EzAbility::Private::ConvertEditorPath(TargetPath_DEPRECATED);
+		TargetPath_DEPRECATED.StructID = FGuid();
+		TargetPath_DEPRECATED.Path.Reset();
+	}
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+#endif // WITH_EDITORONLY_DATA
+
+}
+
+
+//----------------------------------------------------------------//
+//  FEzAbilityPropertyBindings
+//----------------------------------------------------------------//
+
+void FEzAbilityPropertyBindings::Reset()
+{
+	SourceStructs.Reset();
+	CopyBatches.Reset();
+	PropertyPathBindings.Reset();
+	PropertyCopies.Reset();
+	PropertyAccesses.Reset();
+	PropertyReferencePaths.Reset();
+	PropertyIndirections.Reset();
+	
+	bBindingsResolved = false;
+}
+
+const FEzAbilityBindableStructDesc* FEzAbilityPropertyBindings::GetSourceDescByHandle(const FEzAbilityDataHandle SourceDataHandle)
+{
+	TArray<FEzAbilityBindableStructDesc> FoundDescs;
+	for (const FEzAbilityBindableStructDesc& Desc : SourceStructs)
+	{
+		if (Desc.DataHandle == SourceDataHandle)
+		{
+			FoundDescs.Add(Desc);
+		}
+	}
+
+	if (FoundDescs.Num() > 1)
+	{
+		UE_LOG(LogEzAbility, Error, TEXT("%hs: Found %d entries for handle %s."), __FUNCTION__, FoundDescs.Num(), *SourceDataHandle.Describe());
+	}
+	
+	return SourceStructs.FindByPredicate([SourceDataHandle](const FEzAbilityBindableStructDesc& Desc)
+	{
+		return Desc.DataHandle == SourceDataHandle;
+	});
+}
+
+bool FEzAbilityPropertyBindings::ResolvePaths()
+{
+	PropertyIndirections.Reset();
+	PropertyCopies.SetNum(PropertyPathBindings.Num());
+
+	bBindingsResolved = true;
+
+	bool bResult = true;
+	
+	for (const FEzAbilityPropertyCopyBatch& Batch : CopyBatches)
+	{
+		for (int32 i = Batch.BindingsBegin; i != Batch.BindingsEnd; i++)
+		{
+			const FEzAbilityPropertyPathBinding& Binding = PropertyPathBindings[i];
+			
+			FEzAbilityPropertyCopy& Copy = PropertyCopies[i];
+			Copy.SourceDataHandle = Binding.GetSourceDataHandle();
+
+			if (!Binding.GetSourceDataHandle().IsValid())
+			{
+				UE_LOG(LogEzAbility, Error, TEXT("%hs: Invalid source struct for property binding %s."), __FUNCTION__, *Binding.GetSourcePath().ToString());
+				Copy.Type = EEzAbilityPropertyCopyType::None;
+				bBindingsResolved = false;
+				bResult = false;
+				continue;
+			}
+
+			const FEzAbilityBindableStructDesc* SourceDesc = GetSourceDescByHandle(Copy.SourceDataHandle);
+			if (!SourceDesc)
+			{
+				UE_LOG(LogEzAbility, Error, TEXT("%hs: Could not find data source for binding %s."), __FUNCTION__, *Binding.GetSourcePath().ToString());
+				Copy.Type = EEzAbilityPropertyCopyType::None;
+				bBindingsResolved = false;
+				bResult = false;
+				continue;
+			}
+
+			const UStruct* SourceStruct = SourceDesc->Struct;
+			const UStruct* TargetStruct = Batch.TargetStruct.Struct;
+			if (!SourceStruct || !TargetStruct)
+			{
+				Copy.Type = EEzAbilityPropertyCopyType::None;
+				bBindingsResolved = false;
+				bResult = false;
+				continue;
+			}
+
+			Copy.SourceStructType = SourceStruct;
+
+			// Resolve paths and validate the copy. Stops on first failure.
+			bool bSuccess = true;
+			FEzAbilityPropertyPathIndirection SourceLeafIndirection;
+			FEzAbilityPropertyPathIndirection TargetLeafIndirection;
+			bSuccess = bSuccess && ResolvePath(SourceStruct, Binding.GetSourcePath(), Copy.SourceIndirection, SourceLeafIndirection);
+			bSuccess = bSuccess && ResolvePath(TargetStruct, Binding.GetTargetPath(), Copy.TargetIndirection, TargetLeafIndirection);
+			bSuccess = bSuccess && ResolveCopyType(SourceLeafIndirection, TargetLeafIndirection, Copy);
+			if (!bSuccess) 
+			{
+				// Resolving or validating failed, make the copy a nop.
+				Copy.Type = EEzAbilityPropertyCopyType::None;
+				bResult = false;
+			}
+		}
+	}
+
+	PropertyAccesses.Reset();
+	PropertyAccesses.Reserve(PropertyReferencePaths.Num());
+
+	for (const FEzAbilityPropertyRefPath& ReferencePath : PropertyReferencePaths)
+	{
+		FEzAbilityPropertyAccess& PropertyAccess = PropertyAccesses.AddDefaulted_GetRef();
+		
+		PropertyAccess.SourceDataHandle = ReferencePath.GetSourceDataHandle();
+		const FEzAbilityBindableStructDesc* SourceDesc = GetSourceDescByHandle(PropertyAccess.SourceDataHandle);
+		PropertyAccess.SourceStructType = SourceDesc->Struct;
+
+		FEzAbilityPropertyPathIndirection SourceLeafIndirection;
+		if (!ResolvePath(SourceDesc->Struct, ReferencePath.GetSourcePath(), PropertyAccess.SourceIndirection, SourceLeafIndirection))
+		{
+			bResult = false;
+		}
+
+		PropertyAccess.SourceLeafProperty = SourceLeafIndirection.GetProperty();
+	}
+
+	return bResult;
+}
+
+bool FEzAbilityPropertyBindings::ResolvePath(const UStruct* Struct, const FEzAbilityPropertyPath& Path, TArray<FEzAbilityPropertyIndirection>& OutIndirections, FEzAbilityPropertyIndirection& OutFirstIndirection, FEzAbilityPropertyPathIndirection& OutLeafIndirection)
+{
+	if (!Struct)
+ 	{
+		UE_LOG(LogEzAbility, Error, TEXT("%hs: '%s' Invalid source struct."), __FUNCTION__, *Path.ToString());
+		return false;
+	}
+
+	FString Error;
+	TArray<FEzAbilityPropertyPathIndirection> PathIndirections;
+	if (!Path.ResolveIndirections(Struct, PathIndirections, &Error))
+	{
+		UE_LOG(LogEzAbility, Error, TEXT("%hs: %s"), __FUNCTION__, *Error);
+		return false;
+	}
+
+	// Casts array index to FEzAbilityIndex16 (clamping INDEX_NONE to 0) or returns false if out if index bounds.
+	auto CastArrayIndexToIndex16 = [](const int32 Index)
+	{
+		const int32 ClampedIndex = FMath::Max(0, Index);
+		if (!FEzAbilityIndex16::IsValidIndex(ClampedIndex))
+		{
+			return FEzAbilityIndex16();
+		}
+		return FEzAbilityIndex16(ClampedIndex);
+	};
+
+	TArray<FEzAbilityPropertyIndirection, TInlineAllocator<16>> TempIndirections;
+	for (FEzAbilityPropertyPathIndirection& PathIndirection : PathIndirections)
+	{
+		FEzAbilityPropertyIndirection& Indirection = TempIndirections.AddDefaulted_GetRef();
+
+		check(PathIndirection.GetPropertyOffset() >= MIN_uint16 && PathIndirection.GetPropertyOffset() <= MAX_uint16);
+
+		Indirection.Offset = static_cast<uint16>(PathIndirection.GetPropertyOffset());
+		Indirection.Type = PathIndirection.GetAccessType();
+
+		if (Indirection.Type == EEzAbilityPropertyAccessType::IndexArray)
+		{
+			if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(PathIndirection.GetProperty()))
+			{
+				Indirection.ArrayProperty = ArrayProperty;
+				Indirection.ArrayIndex = CastArrayIndexToIndex16(PathIndirection.GetArrayIndex());
+				if (!Indirection.ArrayIndex.IsValid())
+				{
+					UE_LOG(LogEzAbility, Error, TEXT("%hs: Array index %d at %s, is too large."),
+						__FUNCTION__, PathIndirection.GetArrayIndex(), *Path.ToString(PathIndirection.GetPathSegmentIndex(), TEXT("<"), TEXT(">")));
+					return false;
+				}
+			}
+			else
+			{
+				UE_LOG(LogEzAbility, Error, TEXT("%hs: Expect property %s to be array property."),
+					__FUNCTION__, *Path.ToString(PathIndirection.GetPathSegmentIndex(), TEXT("<"), TEXT(">")));
+				return false;
+			}
+		}
+		else if (Indirection.Type == EEzAbilityPropertyAccessType::StructInstance
+				|| Indirection.Type == EEzAbilityPropertyAccessType::ObjectInstance)
+		{
+			if (PathIndirection.GetInstanceStruct())
+			{
+				Indirection.InstanceStruct = PathIndirection.GetInstanceStruct();
+			}
+			else
+			{
+				UE_LOG(LogEzAbility, Error, TEXT("%hs: Expect instanced property access %s to have instance type specified."),
+					__FUNCTION__, *Path.ToString(PathIndirection.GetPathSegmentIndex(), TEXT("<"), TEXT(">")));
+				return false;
+			}
+		}
+	}
+
+	if (TempIndirections.Num() > 0)
+	{
+		for (int32 Index = 0; Index < TempIndirections.Num(); Index++)
+		{
+			FEzAbilityPropertyIndirection& Indirection = TempIndirections[Index];
+			if ((Index + 1) < TempIndirections.Num())
+			{
+				const FEzAbilityPropertyIndirection& NextIndirection = TempIndirections[Index + 1];
+				if (Indirection.Type == EEzAbilityPropertyAccessType::Offset
+					&& NextIndirection.Type == EEzAbilityPropertyAccessType::Offset)
+				{
+					// Collapse adjacent offset indirections
+					Indirection.Offset += NextIndirection.Offset;
+					TempIndirections.RemoveAt(Index + 1);
+					Index--;
+				}
+				else if (Indirection.Type == EEzAbilityPropertyAccessType::IndexArray
+					&& NextIndirection.Type == EEzAbilityPropertyAccessType::Offset
+					&& NextIndirection.Offset == 0)
+				{
+					// Remove empty offset after array indexing.
+					TempIndirections.RemoveAt(Index + 1);
+					Index--;
+				}
+				else if (Indirection.Type == EEzAbilityPropertyAccessType::StructInstance
+					&& NextIndirection.Type == EEzAbilityPropertyAccessType::Offset
+					&& NextIndirection.Offset == 0)
+				{
+					// Remove empty offset after struct indirection.
+					TempIndirections.RemoveAt(Index + 1);
+					Index--;
+				}
+				else if ((Indirection.Type == EEzAbilityPropertyAccessType::Object
+						|| Indirection.Type == EEzAbilityPropertyAccessType::ObjectInstance)
+					&& NextIndirection.Type == EEzAbilityPropertyAccessType::Offset
+					&& NextIndirection.Offset == 0)
+				{
+					// Remove empty offset after object indirection.
+					TempIndirections.RemoveAt(Index + 1);
+					Index--;
+				}
+			}
+		}
+
+		OutLeafIndirection = PathIndirections.Last(); 
+
+		// Store indirections
+		OutFirstIndirection = TempIndirections[0];
+		FEzAbilityPropertyIndirection* PrevIndirection = &OutFirstIndirection;
+		for (int32 Index = 1; Index < TempIndirections.Num(); Index++)
+		{
+			const int32 IndirectionIndex = OutIndirections.Num();
+			PrevIndirection->NextIndex = FEzAbilityIndex16(IndirectionIndex); // Set PrevIndirection before array add, as it can invalidate the pointer.
+			FEzAbilityPropertyIndirection& NewIndirection = OutIndirections.Add_GetRef(TempIndirections[Index]);
+			PrevIndirection = &NewIndirection;
+		}
+	}
+	else
+	{
+		// Indirections can be empty in case we're directly binding to source structs.
+		// Zero offset will return the struct itself.
+		OutFirstIndirection.Offset = 0;
+		OutFirstIndirection.Type = EEzAbilityPropertyAccessType::Offset;
+
+		OutLeafIndirection = FEzAbilityPropertyPathIndirection(Struct);
+	}
+
+	return true;
+}
+
+bool FEzAbilityPropertyBindings::ResolveCopyType(const FEzAbilityPropertyPathIndirection& SourceIndirection,const FEzAbilityPropertyPathIndirection& TargetIndirection, FEzAbilityPropertyCopy& OutCopy)
+{
+	// @todo: see if GetPropertyCompatibility() can be implemented as call to ResolveCopyType() instead so that we write this logic just once.
+	
+	const FProperty* SourceProperty = SourceIndirection.GetProperty();
+	const UStruct* SourceStruct = SourceIndirection.GetContainerStruct();
+	
+	const FProperty* TargetProperty = TargetIndirection.GetProperty();
+	const UStruct* TargetStruct = TargetIndirection.GetContainerStruct();
+
+	if (!SourceStruct || !TargetStruct)
+	{
+		return false;
+	}
+
+	OutCopy.SourceLeafProperty = SourceProperty;
+	OutCopy.TargetLeafProperty = TargetProperty;
+	OutCopy.CopySize = 0;
+	OutCopy.Type = EEzAbilityPropertyCopyType::None;
+	
+	if (SourceProperty == nullptr)
+	{
+		// Copy directly from the source struct, target must be.
+		if (const FStructProperty* TargetStructProperty = CastField<FStructProperty>(TargetProperty))
+		{
+			if (TargetStructProperty->Struct == SourceStruct)
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::CopyStruct;
+				return true;
+			}
+		}
+		else if (const FObjectPropertyBase* TargetObjectProperty = CastField<FObjectPropertyBase>(TargetProperty))
+		{
+			if (SourceStruct->IsChildOf(TargetObjectProperty->PropertyClass))
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::CopyObject;
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	// Handle FEzAbilityStructRef
+	if (const FStructProperty* TargetStructProperty = CastField<const FStructProperty>(TargetProperty))
+	{
+		if (TargetStructProperty->Struct == TBaseStructure<FEzAbilityStructRef>::Get())
+		{
+			if (const FStructProperty* SourceStructProperty = CastField<const FStructProperty>(SourceProperty))
+			{
+				// FEzAbilityStructRef to FEzAbilityStructRef is copied as usual.
+				if (SourceStructProperty->Struct != TBaseStructure<FEzAbilityStructRef>::Get())
+				{
+					OutCopy.Type = EEzAbilityPropertyCopyType::StructReference;
+					return true;
+				}
+			}
+		}
+	}
+
+	const EEzAbilityPropertyAccessCompatibility Compatibility = FEzAbilityPropertyBindings::GetPropertyCompatibility(SourceProperty, TargetProperty);
+
+	// Extract underlying types for enums
+	if (const FEnumProperty* EnumPropertyA = CastField<const FEnumProperty>(SourceProperty))
+	{
+		SourceProperty = EnumPropertyA->GetUnderlyingProperty();
+	}
+
+	if (const FEnumProperty* EnumPropertyB = CastField<const FEnumProperty>(TargetProperty))
+	{
+		TargetProperty = EnumPropertyB->GetUnderlyingProperty();
+	}
+
+	if (Compatibility == EEzAbilityPropertyAccessCompatibility::Compatible)
+	{
+		if (CastField<FNameProperty>(TargetProperty))
+		{
+			OutCopy.Type = EEzAbilityPropertyCopyType::CopyName;
+			return true;
+		}
+		else if (CastField<FBoolProperty>(TargetProperty))
+		{
+			OutCopy.Type = EEzAbilityPropertyCopyType::CopyBool;
+			return true;
+		}
+		else if (CastField<FStructProperty>(TargetProperty))
+		{
+			OutCopy.Type = EEzAbilityPropertyCopyType::CopyStruct;
+			return true;
+		}
+		else if (CastField<FObjectPropertyBase>(TargetProperty))
+		{
+			if (SourceProperty->IsA<FSoftObjectProperty>()
+				&& TargetProperty->IsA<FSoftObjectProperty>())
+			{
+				// Use CopyComplex when copying soft object to another soft object so that we do not try to dereference the object (just copies the path).
+				// This handles soft class too.
+				OutCopy.Type = EEzAbilityPropertyCopyType::CopyComplex;
+			}
+			else
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::CopyObject;
+			}
+			return true;
+		}
+		else if (CastField<FArrayProperty>(TargetProperty) && TargetProperty->HasAnyPropertyFlags(CPF_EditFixedSize))
+		{
+			// only apply array copying rules if the destination array is fixed size, otherwise it will be 'complex'
+			OutCopy.Type = EEzAbilityPropertyCopyType::CopyFixedArray;
+			return true;
+		}
+		else if (TargetProperty->PropertyFlags & CPF_IsPlainOldData)
+		{
+			OutCopy.Type = EEzAbilityPropertyCopyType::CopyPlain;
+			OutCopy.CopySize = SourceProperty->ElementSize * SourceProperty->ArrayDim;
+			return true;
+		}
+		else
+		{
+			OutCopy.Type = EEzAbilityPropertyCopyType::CopyComplex;
+			return true;
+		}
+	}
+	else if (Compatibility == EEzAbilityPropertyAccessCompatibility::Promotable)
+	{
+		if (SourceProperty->IsA<FBoolProperty>())
+		{
+			if (TargetProperty->IsA<FByteProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteBoolToByte;
+				return true;
+			}
+			else if (TargetProperty->IsA<FIntProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteBoolToInt32;
+				return true;
+			}
+			else if (TargetProperty->IsA<FUInt32Property>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteBoolToUInt32;
+				return true;
+			}
+			else if (TargetProperty->IsA<FInt64Property>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteBoolToInt64;
+				return true;
+			}
+			else if (TargetProperty->IsA<FFloatProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteBoolToFloat;
+				return true;
+			}
+			else if (TargetProperty->IsA<FDoubleProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteBoolToDouble;
+				return true;
+			}
+		}
+		else if (SourceProperty->IsA<FByteProperty>())
+		{
+			if (TargetProperty->IsA<FIntProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteByteToInt32;
+				return true;
+			}
+			else if (TargetProperty->IsA<FUInt32Property>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteByteToUInt32;
+				return true;
+			}
+			else if (TargetProperty->IsA<FInt64Property>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteByteToInt64;
+				return true;
+			}
+			else if (TargetProperty->IsA<FFloatProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteByteToFloat;
+				return true;
+			}
+			else if (TargetProperty->IsA<FDoubleProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteByteToDouble;
+				return true;
+			}
+		}
+		else if (SourceProperty->IsA<FIntProperty>())
+		{
+			if (TargetProperty->IsA<FInt64Property>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteInt32ToInt64;
+				return true;
+			}
+			else if (TargetProperty->IsA<FFloatProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteInt32ToFloat;
+				return true;
+			}
+			else if (TargetProperty->IsA<FDoubleProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteInt32ToDouble;
+				return true;
+			}
+		}
+		else if (SourceProperty->IsA<FUInt32Property>())
+		{
+			if (TargetProperty->IsA<FInt64Property>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteUInt32ToInt64;
+				return true;
+			}
+			else if (TargetProperty->IsA<FFloatProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteUInt32ToFloat;
+				return true;
+			}
+			else if (TargetProperty->IsA<FDoubleProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteUInt32ToDouble;
+				return true;
+			}
+		}
+		else if (SourceProperty->IsA<FFloatProperty>())
+		{
+			if (TargetProperty->IsA<FIntProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteFloatToInt32;
+				return true;
+			}
+			else if (TargetProperty->IsA<FInt64Property>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteFloatToInt64;
+				return true;
+			}
+			else if (TargetProperty->IsA<FDoubleProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::PromoteFloatToDouble;
+				return true;
+			}
+		}
+		else if (SourceProperty->IsA<FDoubleProperty>())
+		{
+			if (TargetProperty->IsA<FIntProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::DemoteDoubleToInt32;
+				return true;
+			}
+			else if (TargetProperty->IsA<FInt64Property>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::DemoteDoubleToInt64;
+				return true;
+			}
+			else if (TargetProperty->IsA<FFloatProperty>())
+			{
+				OutCopy.Type = EEzAbilityPropertyCopyType::DemoteDoubleToFloat;
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+EEzAbilityPropertyAccessCompatibility FEzAbilityPropertyBindings::GetPropertyCompatibility(const FProperty* FromProperty, const FProperty* ToProperty)
+{
+	if (FromProperty == ToProperty)
+	{
+		return EEzAbilityPropertyAccessCompatibility::Compatible;
+	}
+
+	if (FromProperty == nullptr || ToProperty == nullptr)
+	{
+		return EEzAbilityPropertyAccessCompatibility::Incompatible;
+	}
+
+	// Special case for object properties since InPropertyA->SameType(InPropertyB) requires both properties to be of the exact same class.
+	// In our case we want to be able to bind a source property if its class is a child of the target property class.
+	if (FromProperty->IsA<FObjectPropertyBase>() && ToProperty->IsA<FObjectPropertyBase>())
+	{
+		const FObjectPropertyBase* SourceProperty = CastField<FObjectPropertyBase>(FromProperty);
+		const FObjectPropertyBase* TargetProperty = CastField<FObjectPropertyBase>(ToProperty);
+		return (SourceProperty->PropertyClass->IsChildOf(TargetProperty->PropertyClass)) ? EEzAbilityPropertyAccessCompatibility::Compatible : EEzAbilityPropertyAccessCompatibility::Incompatible;
+	}
+
+	// When copying to an enum property, expect FromProperty to be the same enum.
+	auto GetPropertyEnum = [](const FProperty* Property) -> const UEnum*
+	{
+		if (const FByteProperty* ByteProperty = CastField<FByteProperty>(Property))
+		{
+			return ByteProperty->GetIntPropertyEnum();
+		}
+		if (const FEnumProperty* EnumProperty = CastField<FEnumProperty>(Property))
+		{
+			return EnumProperty->GetEnum();
+		}
+		return nullptr;
+	};
+	
+	if (const UEnum* ToPropertyEnum = GetPropertyEnum(ToProperty))
+	{
+		const UEnum* FromPropertyEnum = GetPropertyEnum(FromProperty);
+		return (ToPropertyEnum == FromPropertyEnum) ? EEzAbilityPropertyAccessCompatibility::Compatible : EEzAbilityPropertyAccessCompatibility::Incompatible;
+	}
+	
+	// Allow source enums to be promoted to numbers.
+	if (const FEnumProperty* EnumPropertyA = CastField<const FEnumProperty>(FromProperty))
+	{
+		FromProperty = EnumPropertyA->GetUnderlyingProperty();
+	}
+
+	if (FromProperty->SameType(ToProperty))
+	{
+		return EEzAbilityPropertyAccessCompatibility::Compatible;
+	}
+	else
+	{
+		// Not directly compatible, check for promotions
+		if (FromProperty->IsA<FBoolProperty>())
+		{
+			if (ToProperty->IsA<FByteProperty>()
+				|| ToProperty->IsA<FIntProperty>()
+				|| ToProperty->IsA<FUInt32Property>()
+				|| ToProperty->IsA<FInt64Property>()
+				|| ToProperty->IsA<FFloatProperty>()
+				|| ToProperty->IsA<FDoubleProperty>())
+			{
+				return EEzAbilityPropertyAccessCompatibility::Promotable;
+			}
+		}
+		else if (FromProperty->IsA<FByteProperty>())
+		{
+			if (ToProperty->IsA<FIntProperty>()
+				|| ToProperty->IsA<FUInt32Property>()
+				|| ToProperty->IsA<FInt64Property>()
+				|| ToProperty->IsA<FFloatProperty>()
+				|| ToProperty->IsA<FDoubleProperty>())
+			{
+				return EEzAbilityPropertyAccessCompatibility::Promotable;
+			}
+		}
+		else if (FromProperty->IsA<FIntProperty>())
+		{
+			if (ToProperty->IsA<FInt64Property>()
+				|| ToProperty->IsA<FFloatProperty>()
+				|| ToProperty->IsA<FDoubleProperty>())
+			{
+				return EEzAbilityPropertyAccessCompatibility::Promotable;
+			}
+		}
+		else if (FromProperty->IsA<FUInt32Property>())
+		{
+			if (ToProperty->IsA<FInt64Property>()
+				|| ToProperty->IsA<FFloatProperty>()
+				|| ToProperty->IsA<FDoubleProperty>())
+			{
+				return EEzAbilityPropertyAccessCompatibility::Promotable;
+			}
+		}
+		else if (FromProperty->IsA<FFloatProperty>())
+		{
+			if (ToProperty->IsA<FIntProperty>()
+				|| ToProperty->IsA<FInt64Property>()
+				|| ToProperty->IsA<FDoubleProperty>())
+			{
+				return EEzAbilityPropertyAccessCompatibility::Promotable;
+			}
+		}
+		else if (FromProperty->IsA<FDoubleProperty>())
+		{
+			if (ToProperty->IsA<FIntProperty>()
+				|| ToProperty->IsA<FInt64Property>()
+				|| ToProperty->IsA<FFloatProperty>())
+			{
+				return EEzAbilityPropertyAccessCompatibility::Promotable;
+			}
+		}
+	}
+
+	return EEzAbilityPropertyAccessCompatibility::Incompatible;
+}
+
+uint8* FEzAbilityPropertyBindings::GetAddress(FEzAbilityDataView InStructView, TConstArrayView<FEzAbilityPropertyIndirection> Indirections, const FEzAbilityPropertyIndirection& FirstIndirection, const FProperty* LeafProperty)
+{
+	uint8* Address = InStructView.GetMutableMemory();
+	if (Address == nullptr)
+	{
+		// Failed indirection, will be reported by caller.
+		return nullptr;
+	}
+
+	const FEzAbilityPropertyIndirection* Indirection = &FirstIndirection;
+
+	while (Indirection != nullptr && Address != nullptr)
+	{
+		switch (Indirection->Type)
+		{
+		case EEzAbilityPropertyAccessType::Offset:
+		{
+			Address = Address + Indirection->Offset;
+			break;
+		}
+		case EEzAbilityPropertyAccessType::Object:
+		{
+			UObject* Object = *reinterpret_cast<UObject**>(Address + Indirection->Offset);
+			Address = reinterpret_cast<uint8*>(Object);
+			break;
+		}
+		case EEzAbilityPropertyAccessType::WeakObject:
+		{
+			TWeakObjectPtr<UObject>& WeakObjectPtr = *reinterpret_cast<TWeakObjectPtr<UObject>*>(Address + Indirection->Offset);
+			UObject* Object = WeakObjectPtr.Get();
+			Address = reinterpret_cast<uint8*>(Object);
+			break;
+		}
+		case EEzAbilityPropertyAccessType::SoftObject:
+		{
+			FSoftObjectPtr& SoftObjectPtr = *reinterpret_cast<FSoftObjectPtr*>(Address + Indirection->Offset);
+			UObject* Object = SoftObjectPtr.Get();
+			Address = reinterpret_cast<uint8*>(Object);
+			break;
+		}
+		case EEzAbilityPropertyAccessType::ObjectInstance:
+		{
+			check(Indirection->InstanceStruct);
+			UObject* Object = *reinterpret_cast<UObject**>(Address + Indirection->Offset);
+			if (Object
+				&& Object->GetClass()->IsChildOf(Indirection->InstanceStruct))
+			{
+				Address = reinterpret_cast<uint8*>(Object);
+			}
+			else
+			{
+				// Failed indirection, will be reported by caller.
+				return nullptr;
+			}
+			break;
+		}
+		case EEzAbilityPropertyAccessType::StructInstance:
+		{
+			check(Indirection->InstanceStruct);
+			FInstancedStruct& InstancedStruct = *reinterpret_cast<FInstancedStruct*>(Address + Indirection->Offset);
+			const UScriptStruct* InstanceType = InstancedStruct.GetScriptStruct(); 
+			if (InstanceType != nullptr
+				&& InstanceType->IsChildOf(Indirection->InstanceStruct))
+			{
+				Address = InstancedStruct.GetMutableMemory();
+			}
+			else
+			{
+				// Failed indirection, will be reported by caller.
+				return nullptr;
+			}
+			break;
+		}
+		case EEzAbilityPropertyAccessType::IndexArray:
+		{
+			check(Indirection->ArrayProperty);
+			FScriptArrayHelper Helper(Indirection->ArrayProperty, Address + Indirection->Offset);
+			if (Helper.IsValidIndex(Indirection->ArrayIndex.Get()))
+			{
+				Address = Helper.GetRawPtr(Indirection->ArrayIndex.Get());
+			}
+			else
+			{
+				// Failed indirection, will be reported by caller.
+				return nullptr;
+			}
+			break;
+		}
+		default:
+			ensureMsgf(false, TEXT("FEzAbilityPropertyBindings::GetAddress: Unhandled indirection type %s for '%s'"),
+				*StaticEnum<EEzAbilityPropertyAccessType>()->GetValueAsString(Indirection->Type), *LeafProperty->GetNameCPP());
+		}
+
+		Indirection = Indirection->NextIndex.IsValid() ? &Indirections[Indirection->NextIndex.Get()] : nullptr;
+	}
+
+	return Address;
+}
+
+void FEzAbilityPropertyBindings::PerformCopy(const FEzAbilityPropertyCopy& Copy, uint8* SourceAddress, uint8* TargetAddress) const
+{
+	// Source property can be null
+	check(SourceAddress);
+	check(Copy.TargetLeafProperty);
+	check(TargetAddress);
+
+	switch (Copy.Type)
+	{
+	case EEzAbilityPropertyCopyType::CopyPlain:
+		FMemory::Memcpy(TargetAddress, SourceAddress, Copy.CopySize);
+		break;
+	case EEzAbilityPropertyCopyType::CopyComplex:
+		Copy.TargetLeafProperty->CopyCompleteValue(TargetAddress, SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::CopyBool:
+		static_cast<const FBoolProperty*>(Copy.TargetLeafProperty)->SetPropertyValue(TargetAddress, static_cast<const FBoolProperty*>(Copy.SourceLeafProperty)->GetPropertyValue(SourceAddress));
+		break;
+	case EEzAbilityPropertyCopyType::CopyStruct:
+		// If SourceProperty == nullptr (pointing to the struct source directly), the GetAddress() did the right thing and is pointing the the beginning of the struct. 
+		static_cast<const FStructProperty*>(Copy.TargetLeafProperty)->Struct->CopyScriptStruct(TargetAddress, SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::CopyObject:
+		if (Copy.SourceLeafProperty == nullptr)
+		{
+			// Source is pointing at object directly.
+			static_cast<const FObjectPropertyBase*>(Copy.TargetLeafProperty)->SetObjectPropertyValue(TargetAddress, (UObject*)SourceAddress);
+		}
+		else
+		{
+			static_cast<const FObjectPropertyBase*>(Copy.TargetLeafProperty)->SetObjectPropertyValue(TargetAddress, static_cast<const FObjectPropertyBase*>(Copy.SourceLeafProperty)->GetObjectPropertyValue(SourceAddress));
+		}
+		break;
+	case EEzAbilityPropertyCopyType::CopyName:
+		static_cast<const FNameProperty*>(Copy.TargetLeafProperty)->SetPropertyValue(TargetAddress, static_cast<const FNameProperty*>(Copy.SourceLeafProperty)->GetPropertyValue(SourceAddress));
+		break;
+	case EEzAbilityPropertyCopyType::CopyFixedArray:
+	{
+		// Copy into fixed sized array (EditFixedSize). Resizable arrays are copied as Complex, and regular fixed sizes arrays via the regular copies (dim specifies array size).
+		const FArrayProperty* SourceArrayProperty = static_cast<const FArrayProperty*>(Copy.SourceLeafProperty);
+		const FArrayProperty* TargetArrayProperty = static_cast<const FArrayProperty*>(Copy.TargetLeafProperty);
+		FScriptArrayHelper SourceArrayHelper(SourceArrayProperty, SourceAddress);
+		FScriptArrayHelper TargetArrayHelper(TargetArrayProperty, TargetAddress);
+			
+		const int32 MinSize = FMath::Min(SourceArrayHelper.Num(), TargetArrayHelper.Num());
+		for (int32 ElementIndex = 0; ElementIndex < MinSize; ++ElementIndex)
+		{
+			TargetArrayProperty->Inner->CopySingleValue(TargetArrayHelper.GetRawPtr(ElementIndex), SourceArrayHelper.GetRawPtr(ElementIndex));
+		}
+		break;
+	}
+	case EEzAbilityPropertyCopyType::StructReference:
+	{
+		const FStructProperty* SourceStructProperty = static_cast<const FStructProperty*>(Copy.SourceLeafProperty);
+		FEzAbilityStructRef* Target = (FEzAbilityStructRef*)TargetAddress;
+		Target->Set(FStructView(SourceStructProperty->Struct, SourceAddress));
+		break;
+	}
+	// Bool promotions
+	case EEzAbilityPropertyCopyType::PromoteBoolToByte:
+		*reinterpret_cast<uint8*>(TargetAddress) = (uint8)static_cast<const FBoolProperty*>(Copy.SourceLeafProperty)->GetPropertyValue(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteBoolToInt32:
+		*reinterpret_cast<int32*>(TargetAddress) = (int32)static_cast<const FBoolProperty*>(Copy.SourceLeafProperty)->GetPropertyValue(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteBoolToUInt32:
+		*reinterpret_cast<uint32*>(TargetAddress) = (uint32)static_cast<const FBoolProperty*>(Copy.SourceLeafProperty)->GetPropertyValue(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteBoolToInt64:
+		*reinterpret_cast<int64*>(TargetAddress) = (int64)static_cast<const FBoolProperty*>(Copy.SourceLeafProperty)->GetPropertyValue(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteBoolToFloat:
+		*reinterpret_cast<float*>(TargetAddress) = (float)static_cast<const FBoolProperty*>(Copy.SourceLeafProperty)->GetPropertyValue(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteBoolToDouble:
+		*reinterpret_cast<double*>(TargetAddress) = (double)static_cast<const FBoolProperty*>(Copy.SourceLeafProperty)->GetPropertyValue(SourceAddress);
+		break;
+		
+	// Byte promotions	
+	case EEzAbilityPropertyCopyType::PromoteByteToInt32:
+		*reinterpret_cast<int32*>(TargetAddress) = (int32)*reinterpret_cast<const uint8*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteByteToUInt32:
+		*reinterpret_cast<uint32*>(TargetAddress) = (uint32)*reinterpret_cast<const uint8*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteByteToInt64:
+		*reinterpret_cast<int64*>(TargetAddress) = (int64)*reinterpret_cast<const uint8*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteByteToFloat:
+		*reinterpret_cast<float*>(TargetAddress) = (float)*reinterpret_cast<const uint8*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteByteToDouble:
+		*reinterpret_cast<double*>(TargetAddress) = (double)*reinterpret_cast<const uint8*>(SourceAddress);
+		break;
+
+	// Int32 promotions
+	case EEzAbilityPropertyCopyType::PromoteInt32ToInt64:
+		*reinterpret_cast<int64*>(TargetAddress) = (int64)*reinterpret_cast<const int32*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteInt32ToFloat:
+		*reinterpret_cast<float*>(TargetAddress) = (float)*reinterpret_cast<const int32*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteInt32ToDouble:
+		*reinterpret_cast<double*>(TargetAddress) = (double)*reinterpret_cast<const int32*>(SourceAddress);
+		break;
+
+	// Uint32 promotions
+	case EEzAbilityPropertyCopyType::PromoteUInt32ToInt64:
+		*reinterpret_cast<int64*>(TargetAddress) = (int64)*reinterpret_cast<const uint32*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteUInt32ToFloat:
+		*reinterpret_cast<float*>(TargetAddress) = (float)*reinterpret_cast<const uint32*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteUInt32ToDouble:
+		*reinterpret_cast<double*>(TargetAddress) = (double)*reinterpret_cast<const uint32*>(SourceAddress);
+		break;
+
+	// Float promotions
+	case EEzAbilityPropertyCopyType::PromoteFloatToInt32:
+		*reinterpret_cast<int32*>(TargetAddress) = (int32)*reinterpret_cast<const float*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteFloatToInt64:
+		*reinterpret_cast<int64*>(TargetAddress) = (int64)*reinterpret_cast<const float*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::PromoteFloatToDouble:
+		*reinterpret_cast<double*>(TargetAddress) = (double)*reinterpret_cast<const float*>(SourceAddress);
+		break;
+
+	// Double promotions
+	case EEzAbilityPropertyCopyType::DemoteDoubleToInt32:
+		*reinterpret_cast<int32*>(TargetAddress) = (int32)*reinterpret_cast<const double*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::DemoteDoubleToInt64:
+		*reinterpret_cast<int64*>(TargetAddress) = (int64)*reinterpret_cast<const double*>(SourceAddress);
+		break;
+	case EEzAbilityPropertyCopyType::DemoteDoubleToFloat:
+		*reinterpret_cast<float*>(TargetAddress) = (float)*reinterpret_cast<const double*>(SourceAddress);
+		break;
+
+	default:
+		ensureMsgf(false, TEXT("FEzAbilityPropertyBindings::PerformCopy: Unhandled copy type %s between '%s' and '%s'"),
+			*StaticEnum<EEzAbilityPropertyCopyType>()->GetValueAsString(Copy.Type), *Copy.SourceLeafProperty->GetNameCPP(), *Copy.TargetLeafProperty->GetNameCPP());
+		break;
+	}
+}
+
+bool FEzAbilityPropertyBindings::CopyProperty(const FEzAbilityPropertyCopy& Copy, FEzAbilityDataView SourceStructView, FEzAbilityDataView TargetStructView) const
+{
+	// This is made ensure so that the programmers have the change to catch it (it's usually programming error not to call ResolvePaths(), and it wont spam log for others.
+	if (!ensureMsgf(bBindingsResolved, TEXT("Bindings must be resolved successfully before copying. See ResolvePaths()")))
+	{
+		return false;
+	}
+
+	// Copies that fail to be resolved (i.e. property path does not resolve, types changed) will be marked as None, skip them.
+	if (Copy.Type == EEzAbilityPropertyCopyType::None)
+	{
+		return true;
+	}
+
+	bool bResult = true;
+	
+	if (SourceStructView.IsValid() && TargetStructView.IsValid())
+	{
+		check(SourceStructView.GetStruct() == Copy.SourceStructType
+			|| (SourceStructView.GetStruct() && SourceStructView.GetStruct()->IsChildOf(Copy.SourceStructType)));
+			
+		uint8* SourceAddress = GetAddress(SourceStructView, Copy.SourceIndirection, Copy.SourceLeafProperty);
+		uint8* TargetAddress = GetAddress(TargetStructView, Copy.TargetIndirection, Copy.TargetLeafProperty);
+		
+		if (SourceAddress != nullptr && TargetAddress != nullptr)
+		{
+			PerformCopy(Copy, SourceAddress, TargetAddress);
+		}
+		else
+		{
+			bResult = false;
+		}
+	}
+	else
+	{
+		bResult = false;
+	}
+
+	return bResult;
+}
+
+void FEzAbilityPropertyBindings::PerformResetObjects(const FEzAbilityPropertyCopy& Copy, uint8* TargetAddress) const
+{
+	// Source property can be null
+	check(Copy.TargetLeafProperty);
+	check(TargetAddress);
+
+	switch (Copy.Type)
+	{
+	case EEzAbilityPropertyCopyType::CopyComplex:
+		Copy.TargetLeafProperty->InitializeValue(TargetAddress);
+		break;
+	case EEzAbilityPropertyCopyType::CopyStruct:
+		static_cast<const FStructProperty*>(Copy.TargetLeafProperty)->Struct->ClearScriptStruct(TargetAddress);
+		break;
+	case EEzAbilityPropertyCopyType::CopyObject:
+		static_cast<const FObjectPropertyBase*>(Copy.TargetLeafProperty)->SetObjectPropertyValue(TargetAddress, nullptr);
+		break;
+	case EEzAbilityPropertyCopyType::StructReference:
+		reinterpret_cast<FEzAbilityStructRef*>(TargetAddress)->Set(FStructView());
+		break;
+	case EEzAbilityPropertyCopyType::CopyName:
+		break;
+	case EEzAbilityPropertyCopyType::CopyFixedArray:
+	{
+		// Copy into fixed sized array (EditFixedSize). Resizable arrays are copied as Complex, and regular fixed sizes arrays via the regular copies (dim specifies array size).
+		const FArrayProperty* TargetArrayProperty = static_cast<const FArrayProperty*>(Copy.TargetLeafProperty);
+		FScriptArrayHelper TargetArrayHelper(TargetArrayProperty, TargetAddress);
+		for (int32 ElementIndex = 0; ElementIndex < TargetArrayHelper.Num(); ++ElementIndex)
+		{
+			TargetArrayProperty->Inner->InitializeValue(TargetArrayHelper.GetRawPtr(ElementIndex));
+		}
+		break;
+	}
+	default:
+		break;
+	}
+}
+
+const FEzAbilityPropertyAccess* FEzAbilityPropertyBindings::GetPropertyAccess(const FEzAbilityPropertyRef& PropertyRef) const
+{
+	if (!PropertyRef.GetRefAccessIndex().IsValid())
+	{
+		return nullptr;
+	}
+
+	if (!ensure(PropertyAccesses.IsValidIndex(PropertyRef.GetRefAccessIndex().Get())))
+	{
+		return nullptr;
+	}
+
+	return &PropertyAccesses[PropertyRef.GetRefAccessIndex().Get()];
+}
+
+bool FEzAbilityPropertyBindings::ResetObjects(const FEzAbilityIndex16 TargetBatchIndex, FEzAbilityDataView TargetStructView) const
+{
+	// This is made ensure so that the programmers have the change to catch it (it's usually programming error not to call ResolvePaths(), and it wont spam log for others.
+	if (!ensureMsgf(bBindingsResolved, TEXT("Bindings must be resolved successfully before copying. See ResolvePaths()")))
+	{
+		return false;
+	}
+
+	if (TargetBatchIndex.IsValid() == false)
+	{
+		return false;
+	}
+
+	check(CopyBatches.IsValidIndex(TargetBatchIndex.Get()));
+	const FEzAbilityPropertyCopyBatch& Batch = CopyBatches[TargetBatchIndex.Get()];
+
+	check(TargetStructView.IsValid());
+	check(TargetStructView.GetStruct() == Batch.TargetStruct.Struct);
+
+	bool bResult = true;
+	
+	for (int32 i = Batch.BindingsBegin; i != Batch.BindingsEnd; i++)
+	{
+		const FEzAbilityPropertyCopy& Copy = PropertyCopies[i];
+		// Copies that fail to be resolved (i.e. property path does not resolve, types changed) will be marked as None, skip them.
+		if (Copy.Type == EEzAbilityPropertyCopyType::None)
+		{
+			continue;
+		}
+		
+		uint8* TargetAddress = GetAddress(TargetStructView, Copy.TargetIndirection, Copy.TargetLeafProperty);
+		check(TargetAddress != nullptr);
+		PerformResetObjects(Copy, TargetAddress);
+	}
+
+	return bResult;
+}
+
+bool FEzAbilityPropertyBindings::ContainsAnyStruct(const TSet<const UStruct*>& Structs)
+{
+	for (FEzAbilityBindableStructDesc& SourceStruct : SourceStructs)
+	{
+		if (Structs.Contains(SourceStruct.Struct))
+		{
+			return true;
+		}
+	}
+
+	for (FEzAbilityPropertyCopyBatch& CopyBatch : CopyBatches)
+	{
+		if (Structs.Contains(CopyBatch.TargetStruct.Struct))
+		{
+			return true;
+		}
+	}
+
+	auto PathContainsStruct = [&Structs](const FEzAbilityPropertyPath& PropertyPath)
+	{
+		for (const FEzAbilityPropertyPathSegment& Segment : PropertyPath.GetSegments())
+		{
+			if (Structs.Contains(Segment.GetInstanceStruct()))
+			{
+				return true;
+			}
+		}
+		return false;
+	};
+	
+	for (FEzAbilityPropertyPathBinding& PropertyPathBinding : PropertyPathBindings)
+	{
+		if (PathContainsStruct(PropertyPathBinding.GetSourcePath()))
+		{
+			return true;
+		}
+		if (PathContainsStruct(PropertyPathBinding.GetTargetPath()))
+		{
+			return true;
+		}
+	}
+	return false;
+}
+
+void FEzAbilityPropertyBindings::DebugPrintInternalLayout(FString& OutString) const
+{
+	/** Array of expected source structs. */
+	OutString += FString::Printf(TEXT("\nBindableStructDesc (%d)\n  [ %-40s | %-40s ]\n"), SourceStructs.Num(), TEXT("Type"), TEXT("Name"));
+	for (const FEzAbilityBindableStructDesc& BindableStructDesc : SourceStructs)
+	{
+		OutString += FString::Printf(TEXT("  | %-40s | %-40s |\n"),
+									 BindableStructDesc.Struct ? *BindableStructDesc.Struct->GetName() : TEXT("null"),
+									 *BindableStructDesc.Name.ToString());
+	}
+
+	/** Array of copy batches. */
+	OutString += FString::Printf(TEXT("\nCopyBatches (%d)\n  [ %-40s | %-40s | %-8s [%-3s:%-3s[ ]\n"), CopyBatches.Num(),
+		TEXT("Target Type"), TEXT("Target Name"), TEXT("Bindings"), TEXT("Beg"), TEXT("End"));
+	for (const FEzAbilityPropertyCopyBatch& CopyBatch : CopyBatches)
+	{
+		OutString += FString::Printf(TEXT("  | %-40s | %-40s | %8s [%3d:%-3d[ |\n"),
+									 CopyBatch.TargetStruct.Struct ? *CopyBatch.TargetStruct.Struct->GetName() : TEXT("null"),
+									 *CopyBatch.TargetStruct.Name.ToString(),
+									 TEXT(""), CopyBatch.BindingsBegin, CopyBatch.BindingsEnd);
+	}
+
+	/** Array of property bindings, resolved into arrays of copies before use. */
+	OutString += FString::Printf(TEXT("\nPropertyPathBindings (%d)\n"), PropertyPathBindings.Num());
+	for (const FEzAbilityPropertyPathBinding& PropertyBinding : PropertyPathBindings)
+	{
+		OutString += FString::Printf(TEXT("\n  Source: %s | Target: %s"),
+					*PropertyBinding.GetSourcePath().ToString(), *PropertyBinding.GetSourcePath().ToString());
+	}
+
+	/** Array of property copies */
+	OutString += FString::Printf(TEXT("\nPropertyCopies (%d)\n  [ %-7s | %-4s | %-4s | %-10s | %-7s | %-4s | %-4s | %-10s | %-10s | %-20s | %-4s ]\n"), PropertyCopies.Num(),
+		TEXT("Src Idx"), TEXT("Off."), TEXT("Next"), TEXT("Type"),
+		TEXT("Tgt Idx"), TEXT("Off."), TEXT("Next"), TEXT("Type"),
+		TEXT("Source"), TEXT("Copy Type"), TEXT("Size"));
+	for (const FEzAbilityPropertyCopy& PropertyCopy : PropertyCopies)
+	{
+		OutString += FString::Printf(TEXT("  | %7d | %4d | %4d | %-10s | %7d | %4d | %4d | %-10s | %10s | %-20s | %4d |\n"),
+					PropertyCopy.SourceIndirection.ArrayIndex.Get(),
+					PropertyCopy.SourceIndirection.Offset,
+					PropertyCopy.SourceIndirection.NextIndex.Get(),
+					*UEnum::GetDisplayValueAsText(PropertyCopy.SourceIndirection.Type).ToString(),
+					PropertyCopy.TargetIndirection.ArrayIndex.Get(),
+					PropertyCopy.TargetIndirection.Offset,
+					PropertyCopy.TargetIndirection.NextIndex.Get(),
+					*UEnum::GetDisplayValueAsText(PropertyCopy.TargetIndirection.Type).ToString(),
+					*PropertyCopy.SourceDataHandle.Describe(),
+					*UEnum::GetDisplayValueAsText(PropertyCopy.Type).ToString(),
+					PropertyCopy.CopySize);
+	}
+
+	/** Array of property indirections, indexed by accesses*/
+	OutString += FString::Printf(TEXT("\nPropertyIndirections (%d)\n  [ %-4s | %-4s | %-4s | %-10s ] \n"), PropertyIndirections.Num(),
+		TEXT("Idx"), TEXT("Off."), TEXT("Next"), TEXT("Access Type"));
+	for (const FEzAbilityPropertyIndirection& PropertyIndirection : PropertyIndirections)
+	{
+		OutString += FString::Printf(TEXT("  | %4d | %4d | %4d | %-10s |\n"),
+					PropertyIndirection.ArrayIndex.Get(),
+					PropertyIndirection.Offset,
+					PropertyIndirection.NextIndex.Get(),
+					*UEnum::GetDisplayValueAsText(PropertyIndirection.Type).ToString());
+	}
+}
+
+
+//----------------------------------------------------------------//
+//  FEzAbilityPropertyPath
+//----------------------------------------------------------------//
+
+bool FEzAbilityPropertyPath::FromString(const FString& InPath)
+{
+	Segments.Reset();
+	
+	if (InPath.IsEmpty())
+	{
+		return true;
+	}
+	
+	bool bResult = true;
+	TArray<FString> PathSegments;
+	InPath.ParseIntoArray(PathSegments, TEXT("."), /*InCullEmpty=*/false);
+	
+	for (const FString& Segment : PathSegments)
+	{
+		if (Segment.IsEmpty())
+		{
+			bResult = false;
+			break;
+		}
+
+		int32 FirstBracket = INDEX_NONE;
+		int32 LastBracket = INDEX_NONE;
+		if (Segment.FindChar(TEXT('['), FirstBracket)
+			&& Segment.FindLastChar(TEXT(']'), LastBracket))
+		{
+			const int32 NameStringLength = FirstBracket;
+			const int32 IndexStringLength = LastBracket - FirstBracket - 1;
+			if (NameStringLength < 1
+				|| IndexStringLength <= 0)
+			{
+				bResult = false;
+				break;
+			}
+
+			const FString NameString = Segment.Left(FirstBracket);
+			const FString IndexString = Segment.Mid(FirstBracket + 1, IndexStringLength);
+			int32 ArrayIndex = INDEX_NONE;
+			LexFromString(ArrayIndex, *IndexString);
+			if (ArrayIndex < 0)
+			{
+				bResult = false;
+				break;
+			}
+			
+			AddPathSegment(FName(NameString), ArrayIndex);
+		}
+		else
+		{
+			AddPathSegment(FName(Segment));
+		}
+	}
+
+	if (!bResult)
+	{
+		Segments.Reset();
+	}
+	
+	return bResult;
+}
+
+bool FEzAbilityPropertyPath::UpdateSegments(const UStruct* BaseStruct, FString* OutError)
+{
+	return UpdateSegmentsFromValue(FEzAbilityDataView(BaseStruct, nullptr), OutError);
+}
+
+bool FEzAbilityPropertyPath::UpdateSegmentsFromValue(const FEzAbilityDataView BaseValueView, FString* OutError)
+{
+	TArray<FEzAbilityPropertyPathIndirection> Indirections;
+	if (!ResolveIndirectionsWithValue(BaseValueView, Indirections, OutError, /*bHandleRedirects*/true))
+	{
+		return false;
+	}
+
+	for (FEzAbilityPropertyPathSegment& Segment : Segments)
+	{
+		Segment.SetInstanceStruct(nullptr);
+	}
+	
+	for (const FEzAbilityPropertyPathIndirection& Indirection : Indirections)
+	{
+		if (Indirection.InstanceStruct != nullptr)
+		{
+			Segments[Indirection.PathSegmentIndex].SetInstanceStruct(Indirection.InstanceStruct);
+		}
+#if WITH_EDITORONLY_DATA		
+		if (!Indirection.GetRedirectedName().IsNone())
+		{
+			Segments[Indirection.PathSegmentIndex].SetName(Indirection.GetRedirectedName());
+		}
+		Segments[Indirection.PathSegmentIndex].SetPropertyGuid(Indirection.GetPropertyGuid());
+#endif			
+	}
+
+	return true;
+}
+
+FString FEzAbilityPropertyPath::ToString(const int32 HighlightedSegment, const TCHAR* HighlightPrefix, const TCHAR* HighlightPostfix, const bool bOutputInstances) const
+{
+	FString Result;
+	for (TEnumerateRef<const FEzAbilityPropertyPathSegment> Segment : EnumerateRange(Segments))
+	{
+		if (Segment.GetIndex() > 0)
+		{
+			Result += TEXT(".");
+		}
+		if (Segment.GetIndex() == HighlightedSegment && HighlightPrefix)
+		{
+			Result += HighlightPrefix;
+		}
+
+		if (bOutputInstances && Segment->GetInstanceStruct())
+		{
+			Result += FString::Printf(TEXT("(%s)"), *GetNameSafe(Segment->GetInstanceStruct()));
+		}
+
+		if (Segment->GetArrayIndex() >= 0)
+		{
+			Result += FString::Printf(TEXT("%s[%d]"), *Segment->GetName().ToString(), Segment->GetArrayIndex());
+		}
+		else
+		{
+			Result += Segment->GetName().ToString();
+		}
+
+		if (Segment.GetIndex() == HighlightedSegment && HighlightPostfix)
+		{
+			Result += HighlightPostfix;
+		}
+	}
+	return Result;
+}
+
+
+bool FEzAbilityPropertyPath::ResolveIndirections(const UStruct* BaseStruct, TArray<FEzAbilityPropertyPathIndirection>& OutIndirections, FString* OutError, bool bHandleRedirects) const
+{
+	return ResolveIndirectionsWithValue(FEzAbilityDataView(BaseStruct, nullptr), OutIndirections, OutError, bHandleRedirects);
+}
+
+bool FEzAbilityPropertyPath::ResolveIndirectionsWithValue(const FEzAbilityDataView BaseValueView, TArray<FEzAbilityPropertyPathIndirection>& OutIndirections, FString* OutError, bool bHandleRedirects) const
+{
+	OutIndirections.Reset();
+	if (OutError)
+	{
+		OutError->Reset();
+	}
+	
+	// Nothing to do for an empty path.
+	if (IsPathEmpty())
+	{
+		return true;
+	}
+
+	const uint8* CurrentAddress = BaseValueView.GetMemory();
+	const UStruct* CurrentStruct = BaseValueView.GetStruct();
+	
+	for (const TEnumerateRef<const FEzAbilityPropertyPathSegment> Segment : EnumerateRange(Segments))
+	{
+		if (CurrentStruct == nullptr)
+		{
+			if (OutError)
+			{
+				*OutError = FString::Printf(TEXT("Malformed path '%s'."),
+					*ToString(Segment.GetIndex(), TEXT("<"), TEXT(">")));
+			}
+			OutIndirections.Reset();
+			return false;
+		}
+
+		const FProperty* Property = CurrentStruct->FindPropertyByName(Segment->GetName());
+		const bool bWithValue = CurrentAddress != nullptr;
+
+#if WITH_EDITORONLY_DATA
+		FName RedirectedName;
+		FGuid PropertyGuid = Segment->GetPropertyGuid();
+
+		// Try to fix the path in editor.
+		if (bHandleRedirects)
+		{
+			
+			// Check if there's a core redirect for it.
+			if (!Property)
+			{
+				// Try to match by property ID (Blueprint or User Defined Struct).
+				if (Segment->GetPropertyGuid().IsValid())
+				{
+					if (const UBlueprintGeneratedClass* BlueprintClass = Cast<UBlueprintGeneratedClass>(CurrentStruct))
+					{
+						if (const FName* Name = BlueprintClass->PropertyGuids.FindKey(Segment->GetPropertyGuid()))
+						{
+							RedirectedName = *Name;
+							Property = CurrentStruct->FindPropertyByName(RedirectedName);
+						}
+					}
+					else if (const UUserDefinedStruct* UserDefinedStruct = Cast<UUserDefinedStruct>(CurrentStruct))
+					{
+						if (FProperty* FoundProperty = FStructureEditorUtils::GetPropertyByGuid(UserDefinedStruct, Segment->GetPropertyGuid()))
+						{
+							RedirectedName = FoundProperty->GetFName();
+							Property = FoundProperty;
+						}
+					}
+					else if (const UPropertyBag* PropertyBag = Cast<UPropertyBag>(CurrentStruct))
+					{
+						if (const FPropertyBagPropertyDesc* Desc = PropertyBag->FindPropertyDescByID(Segment->GetPropertyGuid()))
+						{
+							if (Desc->CachedProperty)
+							{
+								RedirectedName = Desc->CachedProperty->GetFName();
+								Property = Desc->CachedProperty;
+							}
+						}
+					}
+				}
+				else
+				{
+					// Try core redirect
+					const FCoreRedirectObjectName OldPropertyName(Segment->GetName(), CurrentStruct->GetFName(), *CurrentStruct->GetOutermost()->GetPathName());
+					const FCoreRedirectObjectName NewPropertyName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Property, OldPropertyName);
+					if (OldPropertyName != NewPropertyName)
+					{
+						// Cached the result for later use.
+						RedirectedName = NewPropertyName.ObjectName;
+
+						Property = CurrentStruct->FindPropertyByName(RedirectedName);
+					}
+				}
+			}
+
+			// Update PropertyGuid 
+			if (Property)
+			{
+				const FName PropertyName = !RedirectedName.IsNone() ? RedirectedName : Segment->GetName();
+				if (const UBlueprintGeneratedClass* BlueprintClass = Cast<UBlueprintGeneratedClass>(CurrentStruct))
+				{
+					if (const FGuid* VarGuid = BlueprintClass->PropertyGuids.Find(PropertyName))
+					{
+						PropertyGuid = *VarGuid;
+					}
+				}
+				else if (const UUserDefinedStruct* UserDefinedStruct = Cast<UUserDefinedStruct>(CurrentStruct))
+				{
+					// Parse Guid from UDS property name.
+					PropertyGuid = FStructureEditorUtils::GetGuidFromPropertyName(PropertyName);
+				}
+				else if (const UPropertyBag* PropertyBag = Cast<UPropertyBag>(CurrentStruct))
+				{
+					if (const FPropertyBagPropertyDesc* Desc = PropertyBag->FindPropertyDescByPropertyName(PropertyName))
+					{
+						PropertyGuid = Desc->ID;
+					}
+				}
+			}
+		}
+#endif // WITH_EDITORONLY_DATA
+
+		if (!Property)
+		{
+			if (OutError)
+			{
+				*OutError = FString::Printf(TEXT("Malformed path '%s', could not find property '%s%s::%s'."),
+					*ToString(Segment.GetIndex(), TEXT("<"), TEXT(">")),
+					CurrentStruct->GetPrefixCPP(), *CurrentStruct->GetName(), *Segment->GetName().ToString());
+			}
+			OutIndirections.Reset();
+			return false;
+		}
+
+		const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property);
+		int ArrayIndex = 0;
+		int32 Offset = 0;
+		if (ArrayProperty && Segment->GetArrayIndex() != INDEX_NONE)
+		{
+			FEzAbilityPropertyPathIndirection& Indirection = OutIndirections.AddDefaulted_GetRef();
+			Indirection.Property = Property;
+			Indirection.ContainerAddress = CurrentAddress;
+			Indirection.ContainerStruct = CurrentStruct;
+			Indirection.InstanceStruct = nullptr;
+			Indirection.ArrayIndex = Segment->GetArrayIndex();
+			Indirection.PropertyOffset = ArrayProperty->GetOffset_ForInternal();
+			Indirection.PathSegmentIndex = Segment.GetIndex();
+			Indirection.AccessType = EEzAbilityPropertyAccessType::IndexArray;
+#if WITH_EDITORONLY_DATA
+			Indirection.RedirectedName = RedirectedName;
+			Indirection.PropertyGuid = PropertyGuid;
+#endif
+			
+			ArrayIndex = 0;
+			Offset = 0;
+			Property = ArrayProperty->Inner;
+
+			if (bWithValue)
+			{
+				FScriptArrayHelper Helper(ArrayProperty, CurrentAddress + ArrayProperty->GetOffset_ForInternal());
+				if (!Helper.IsValidIndex(Segment->GetArrayIndex()))
+				{
+					if (OutError)
+					{
+						*OutError = FString::Printf(TEXT("Index %d out of range (num elements %d) trying to access dynamic array '%s'."),
+							Segment->GetArrayIndex(), Helper.Num(), *ToString(Segment.GetIndex(), TEXT("<"), TEXT(">")));
+					}
+					OutIndirections.Reset();
+					return false;
+				}
+				CurrentAddress = Helper.GetRawPtr(Segment->GetArrayIndex());
+			}
+		}
+		else
+		{
+			if (Segment->GetArrayIndex() > Property->ArrayDim)
+			{
+				if (OutError)
+				{
+					*OutError = FString::Printf(TEXT("Index %d out of range %d trying to access static array '%s'."),
+						Segment->GetArrayIndex(), Property->ArrayDim, *ToString(Segment.GetIndex(), TEXT("<"), TEXT(">")));
+				}
+				OutIndirections.Reset();
+				return false;
+			}
+			ArrayIndex = FMath::Max(0, Segment->GetArrayIndex());
+			Offset = Property->GetOffset_ForInternal() + Property->ElementSize * ArrayIndex;
+		}
+
+		FEzAbilityPropertyPathIndirection& Indirection = OutIndirections.AddDefaulted_GetRef();
+		Indirection.Property = Property;
+		Indirection.ContainerAddress = CurrentAddress;
+		Indirection.ContainerStruct = CurrentStruct;
+		Indirection.ArrayIndex = ArrayIndex;
+		Indirection.PropertyOffset = Offset;
+		Indirection.PathSegmentIndex = Segment.GetIndex();
+		Indirection.AccessType = EEzAbilityPropertyAccessType::Offset; 
+#if WITH_EDITORONLY_DATA
+		Indirection.RedirectedName = RedirectedName;
+		Indirection.PropertyGuid = PropertyGuid;
+#endif
+		const bool bLastSegment = Segment.GetIndex() == (Segments.Num() - 1);
+
+		if (!bLastSegment)
+		{
+			if (const FStructProperty* StructProperty = CastField<FStructProperty>(Property))
+			{
+				if (bWithValue)
+				{
+					if (StructProperty->Struct == TBaseStructure<FInstancedStruct>::Get())
+					{
+						// The property path is pointing into the instanced struct, it must be present.
+						// @TODO:	We could potentially check the BaseStruct metadata in editor (for similar behavior as objects)
+						//			Omitting for now to have matching functionality in editor and runtime.
+						const FInstancedStruct& InstancedStruct = *reinterpret_cast<const FInstancedStruct*>(CurrentAddress + Offset);
+						if (!InstancedStruct.IsValid())
+						{
+							if (OutError)
+							{
+								*OutError = FString::Printf(TEXT("Expecting valid instanced struct value at path '%s'."),
+									*ToString(Segment.GetIndex(), TEXT("<"), TEXT(">")));
+							}
+							OutIndirections.Reset();
+							return false;
+						}
+						const UScriptStruct* ValueInstanceStructType = InstancedStruct.GetScriptStruct();
+
+						CurrentAddress = InstancedStruct.GetMemory();
+						CurrentStruct = ValueInstanceStructType;
+						Indirection.InstanceStruct = CurrentStruct;
+						Indirection.AccessType = EEzAbilityPropertyAccessType::StructInstance; 
+					}
+					else
+					{
+						CurrentAddress = CurrentAddress + Offset;
+						CurrentStruct = StructProperty->Struct;
+						Indirection.AccessType = EEzAbilityPropertyAccessType::Offset;
+					}
+				}
+				else
+				{
+					if (Segment->GetInstanceStruct())
+					{
+						CurrentStruct = Segment->GetInstanceStruct();
+						Indirection.InstanceStruct = CurrentStruct;
+						Indirection.AccessType = EEzAbilityPropertyAccessType::StructInstance;
+					}
+					else
+					{
+						CurrentStruct = StructProperty->Struct;
+						Indirection.AccessType = EEzAbilityPropertyAccessType::Offset;
+					}
+				}
+			}
+			else if (const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(Property))
+			{
+				if (bWithValue)
+				{
+					const UObject* Object = *reinterpret_cast<UObject* const*>(CurrentAddress + Offset);
+					CurrentAddress = reinterpret_cast<const uint8*>(Object);
+					
+					// The property path is pointing into the object, if the object is present use it's specific type, otherwise use the type of the pointer.
+					if (Object)
+					{
+						CurrentStruct = Object->GetClass();
+						Indirection.InstanceStruct = CurrentStruct;
+						Indirection.AccessType = EEzAbilityPropertyAccessType::ObjectInstance;
+					}
+					else
+					{
+						CurrentStruct = ObjectProperty->PropertyClass;
+						Indirection.AccessType = EEzAbilityPropertyAccessType::Object;
+					}
+				}
+				else
+				{
+					if (Segment->GetInstanceStruct())
+					{
+						CurrentStruct = Segment->GetInstanceStruct();
+						Indirection.InstanceStruct = CurrentStruct;
+						Indirection.AccessType = EEzAbilityPropertyAccessType::ObjectInstance;
+					}
+					else
+					{
+						CurrentStruct = ObjectProperty->PropertyClass;
+						Indirection.AccessType = EEzAbilityPropertyAccessType::Object;
+					}
+				}
+			}
+			// Check to see if this is a simple weak object property (eg. not an array of weak objects).
+			else if (const FWeakObjectProperty* WeakObjectProperty = CastField<FWeakObjectProperty>(Property))
+			{
+				if (bWithValue)
+				{
+					const TWeakObjectPtr<UObject>& WeakObjectPtr = *reinterpret_cast<const TWeakObjectPtr<UObject>*>(CurrentAddress + Offset);
+					const UObject* Object = WeakObjectPtr.Get();
+					CurrentAddress = reinterpret_cast<const uint8*>(Object);
+
+					if (Object)
+					{
+						CurrentStruct = Object->GetClass();
+						Indirection.InstanceStruct = CurrentStruct;
+					}
+				}
+				else
+				{
+					CurrentStruct = WeakObjectProperty->PropertyClass;
+				}
+				
+				Indirection.AccessType = EEzAbilityPropertyAccessType::WeakObject;
+			}
+			// Check to see if this is a simple soft object property (eg. not an array of soft objects).
+			else if (const FSoftObjectProperty* SoftObjectProperty = CastField<FSoftObjectProperty>(Property))
+			{
+				if (bWithValue)
+				{
+					const FSoftObjectPtr& SoftObjectPtr = *reinterpret_cast<const FSoftObjectPtr*>(CurrentAddress + Offset);
+					const UObject* Object = SoftObjectPtr.Get();
+					CurrentAddress = reinterpret_cast<const uint8*>(Object);
+
+					if (Object)
+					{
+						CurrentStruct = Object->GetClass();
+						Indirection.InstanceStruct = CurrentStruct;
+					}			
+				}
+				else
+				{			
+					CurrentStruct = SoftObjectProperty->PropertyClass;
+				}
+
+				Indirection.AccessType = EEzAbilityPropertyAccessType::SoftObject;
+			}
+			else
+			{
+				// We get here if we encounter a property type that is not supported for indirection (e.g. Map or Set).
+				if (OutError)
+				{
+					*OutError = FString::Printf(TEXT("Unsupported property indirection type %s in path '%s'."),
+						*Property->GetCPPType(), *ToString(Segment.GetIndex(), TEXT("<"), TEXT(">")));
+				}
+				OutIndirections.Reset();
+				return false;
+			}
+		}
+	}
+
+	return true;
+}
+
+bool FEzAbilityPropertyPath::operator==(const FEzAbilityPropertyPath& RHS) const
+{
+#if WITH_EDITORONLY_DATA
+	if (StructID != RHS.StructID)
+	{
+		return false;
+	}
+#endif // WITH_EDITORONLY_DATA
+	if (Segments.Num() != RHS.Segments.Num())
+	{
+		return false;
+	}
+
+	for (TEnumerateRef<const FEzAbilityPropertyPathSegment> Segment : EnumerateRange(Segments))
+	{
+		if (*Segment != RHS.Segments[Segment.GetIndex()])
+		{
+			return false;
+		}
+	}
+
+	return true;
+}
+
+
+//----------------------------------------------------------------//
+//  FEzAbilityEditorPropertyPath
+//----------------------------------------------------------------//
+
+FString FEzAbilityEditorPropertyPath::ToString(const int32 HighlightedSegment, const TCHAR* HighlightPrefix, const TCHAR* HighlightPostfix) const
+{
+	FString Result;
+	for (int32 i = 0; i < Path.Num(); i++)
+	{
+		if (i > 0)
+		{
+			Result += TEXT(".");
+		}
+		if (i == HighlightedSegment && HighlightPrefix)
+		{
+			Result += HighlightPrefix;
+		}
+
+		Result += Path[i];
+
+		if (i == HighlightedSegment && HighlightPostfix)
+		{
+			Result += HighlightPostfix;
+		}
+	}
+	return Result;
+}
+
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+bool FEzAbilityEditorPropertyPath::operator==(const FEzAbilityEditorPropertyPath& RHS) const
+{
+	if (StructID != RHS.StructID)
+	{
+		return false;
+	}
+
+	if (Path.Num() != RHS.Path.Num())
+	{
+		return false;
+	}
+
+	for (int32 i = 0; i < Path.Num(); i++)
+	{
+		if (Path[i] != RHS.Path[i])
+		{
+			return false;
+		}
+	}
+
+	return true;
+}
+PRAGMA_ENABLE_DEPRECATION_WARNINGS

+ 1 - 1
Ability/Plugins/EzAbility/Source/EzAbility/Private/Transition/EzAbilityTransition.cpp → Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityPropertyRef.cpp

@@ -1,4 +1,4 @@
 // Fill out your copyright notice in the Description page of Project Settings.
 
 
-#include "Transition/EzAbilityTransition.h"
+#include "EzAbilityPropertyRef.h"

+ 279 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityPropertyRefHelpers.cpp

@@ -0,0 +1,279 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#if WITH_EDITOR
+#include "EzAbilityPropertyRefHelpers.h"
+#include "EzAbilityPropertyRef.h"
+#include "UObject/TextProperty.h"
+#include "UObject/EnumProperty.h"
+#include "UObject/Class.h"
+#include "IPropertyAccessEditor.h"
+#include "EzAbilityPropertyBindings.h"
+#include "EdGraph/EdGraphPin.h"
+#include "EdGraphSchema_K2.h"
+
+namespace UE::EzAbility::PropertyRefHelpers
+{
+	static const FName BoolName = TEXT("bool");
+	static const FName ByteName = TEXT("byte");
+	static const FName Int32Name = TEXT("int32");
+	static const FName Int64Name = TEXT("int64");
+	static const FName FloatName = TEXT("float");
+	static const FName DoubleName = TEXT("double");
+	static const FName NameName = TEXT("Name");
+	static const FName StringName = TEXT("String");
+	static const FName TextName = TEXT("Text");
+
+	static const FName IsRefToArrayName = TEXT("IsRefToArray");
+	static const FName RefTypeName = TEXT("RefType");
+
+	bool IsPropertyRefCompatibleWithProperty(const FProperty& RefProperty, const FProperty& SourceProperty)
+	{
+		ensure(IsPropertyRef(RefProperty));
+
+		const FProperty* TestProperty = &SourceProperty;
+		const bool bIsTargetRefArray = RefProperty.HasMetaData(IsRefToArrayName);
+
+		if (bIsTargetRefArray)
+		{
+			if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(TestProperty))
+			{
+				TestProperty = ArrayProperty->Inner;
+			}
+			else
+			{
+				return false;
+			}
+		}
+
+		const FString& TargetTypeNameStr = RefProperty.GetMetaData(RefTypeName);
+		if (TargetTypeNameStr.IsEmpty())
+		{
+			return false;
+		}
+
+		const FName TargetTypeName = FName(*TargetTypeNameStr);
+
+		const FStructProperty* SourceStructProperty = CastField<FStructProperty>(TestProperty);
+
+		// Compare properties metadata directly if SourceProperty is PropertyRef as well
+		if (SourceStructProperty && SourceStructProperty->Struct == FEzAbilityPropertyRef::StaticStruct())
+		{
+			const FName SourceTypeName(SourceStructProperty->GetMetaData(RefTypeName));
+			const bool bIsSourceRefArray = SourceStructProperty->GetBoolMetaData(IsRefToArrayName);
+
+			return SourceTypeName == TargetTypeName && bIsSourceRefArray == bIsTargetRefArray;
+		}
+
+		if(TargetTypeName == BoolName)
+		{
+			return CastField<FBoolProperty>(TestProperty) != nullptr;
+		}
+		else if(TargetTypeName == ByteName)
+		{
+			return CastField<FByteProperty>(TestProperty) != nullptr;
+		}
+		else if(TargetTypeName == Int32Name)
+		{
+			return CastField<FIntProperty>(TestProperty) != nullptr;
+		}
+		else if(TargetTypeName == Int64Name)
+		{
+			return CastField<FInt64Property>(TestProperty) != nullptr;
+		}
+		else if(TargetTypeName == FloatName)
+		{
+			return CastField<FFloatProperty>(TestProperty) != nullptr;
+		}
+		else if(TargetTypeName == DoubleName)
+		{
+			return CastField<FDoubleProperty>(TestProperty) != nullptr;
+		}
+		else if(TargetTypeName == NameName)
+		{
+			return CastField<FNameProperty>(TestProperty) != nullptr;
+		}
+		else if(TargetTypeName == StringName)
+		{
+			return CastField<FStrProperty>(TestProperty) != nullptr;
+		}
+		else if(TargetTypeName == TextName)
+		{
+			return CastField<FTextProperty>(TestProperty) != nullptr;
+		}
+		else
+		{
+			UField* TargetRefField = UClass::TryFindTypeSlow<UField>(TargetTypeNameStr);
+			if (!TargetRefField)
+			{
+				TargetRefField = LoadObject<UField>(nullptr, *TargetTypeNameStr);
+			}
+
+			if (SourceStructProperty)
+			{
+				return SourceStructProperty->Struct->IsChildOf(Cast<UStruct>(TargetRefField));
+			}
+
+			if (const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(TestProperty))
+			{
+				// Only referencing object of the same exact class should be allowed. Otherwise one could e.g assign UObject to AActor property through reference to UObject.
+				return ObjectProperty->PropertyClass == Cast<UStruct>(TargetRefField);
+			}
+
+			if (const FEnumProperty* EnumProperty = CastField<FEnumProperty>(TestProperty))
+			{
+				return EnumProperty->GetEnum() == TargetRefField;
+			}
+		}
+
+		return false;
+	}
+
+	bool IsPropertyAccessibleForPropertyRef(const FProperty& SourceProperty, FEzAbilityBindableStructDesc SourceStruct, bool bIsOutput)
+	{
+		switch (SourceStruct.DataSource)
+		{
+		case EEzAbilityBindableStructSource::Parameter:
+		case EEzAbilityBindableStructSource::State:
+			return true;
+
+		case EEzAbilityBindableStructSource::Context:
+		case EEzAbilityBindableStructSource::Condition:
+			return false;
+
+		case EEzAbilityBindableStructSource::GlobalTask:
+		case EEzAbilityBindableStructSource::Evaluator:
+		case EEzAbilityBindableStructSource::Task:
+			return bIsOutput || IsPropertyRef(SourceProperty);
+		default:
+			checkNoEntry();
+		}
+
+		return false;
+	}
+
+	bool IsPropertyAccessibleForPropertyRef(TConstArrayView<FEzAbilityPropertyPathIndirection> SourcePropertyPathIndirections, FEzAbilityBindableStructDesc SourceStruct)
+	{
+		bool bIsOutput = false;
+		for (const FEzAbilityPropertyPathIndirection& Indirection : SourcePropertyPathIndirections)
+		{
+			if (UE::EzAbility::GetUsageFromMetaData(Indirection.GetProperty()) == EEzAbilityPropertyUsage::Output)
+			{
+				bIsOutput = true;
+				break;
+			}
+		}
+
+		return IsPropertyAccessibleForPropertyRef(*SourcePropertyPathIndirections.Last().GetProperty(), SourceStruct, bIsOutput);
+	}
+
+	bool IsPropertyAccessibleForPropertyRef(const FProperty& SourceProperty, TConstArrayView<FBindingChainElement> BindingChain, FEzAbilityBindableStructDesc SourceStruct)
+	{
+		bool bIsOutput = UE::EzAbility::GetUsageFromMetaData(&SourceProperty) == EEzAbilityPropertyUsage::Output;
+		for (const FBindingChainElement& ChainElement : BindingChain)
+		{
+			if (const FProperty* Property = ChainElement.Field.Get<FProperty>())
+			{
+				if (UE::EzAbility::GetUsageFromMetaData(Property) == EEzAbilityPropertyUsage::Output)
+				{
+					bIsOutput = true;
+					break;
+				}
+			}
+		}
+
+		return IsPropertyAccessibleForPropertyRef(SourceProperty, SourceStruct, bIsOutput);
+	}
+
+	FEdGraphPinType GetPropertyRefInternalTypeAsPin(const FProperty& RefProperty)
+	{
+		ensure(IsPropertyRef(RefProperty));
+
+		FEdGraphPinType PinType;
+		PinType.PinSubCategory = NAME_None;
+
+		PinType.ContainerType = RefProperty.HasMetaData(IsRefToArrayName) ? EPinContainerType::Array : EPinContainerType::None;
+		const FString& TargetTypeNameStr = RefProperty.GetMetaData(RefTypeName);
+		const FName TargetTypeName = FName(*TargetTypeNameStr);
+
+		if(TargetTypeName == BoolName)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_Boolean;
+		}
+		else if(TargetTypeName == ByteName)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_Byte;
+		}
+		else if(TargetTypeName == Int32Name)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_Int;
+		}
+		else if(TargetTypeName == Int64Name)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_Int64;
+		}
+		else if(TargetTypeName == FloatName)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_Real;
+			PinType.PinSubCategory = UEdGraphSchema_K2::PC_Float;
+		}
+		else if(TargetTypeName == DoubleName)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_Real;
+			PinType.PinSubCategory = UEdGraphSchema_K2::PC_Double;
+		}
+		else if(TargetTypeName == NameName)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_Name;
+		}
+		else if(TargetTypeName == StringName)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_String;
+		}
+		else if(TargetTypeName == TextName)
+		{
+			PinType.PinCategory = UEdGraphSchema_K2::PC_Text;
+		}
+		else
+		{
+			UField* TargetRefField = UClass::TryFindTypeSlow<UField>(TargetTypeNameStr);
+			if (!TargetRefField)
+			{
+				TargetRefField = LoadObject<UField>(nullptr, *TargetTypeNameStr);
+			}
+
+			if (UStruct* Struct = Cast<UStruct>(TargetRefField))
+			{
+				PinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
+				PinType.PinSubCategoryObject = Struct;
+			}
+			else if (UObject* Object = Cast<UObject>(TargetRefField))
+			{
+				PinType.PinCategory = UEdGraphSchema_K2::PC_Object;
+				PinType.PinSubCategoryObject = Object;
+			}
+			else if (UEnum* Enum = Cast<UEnum>(TargetRefField))
+			{
+				PinType.PinCategory = UEdGraphSchema_K2::PC_Enum;
+				PinType.PinSubCategoryObject = Enum;
+			}
+			else
+			{
+				checkNoEntry();
+			}
+		}
+
+		return PinType;
+	}
+
+	bool IsPropertyRef(const FProperty& Property)
+	{
+		if (const FStructProperty* StructProperty = CastField<FStructProperty>(&Property))
+		{
+			return StructProperty->Struct == FEzAbilityPropertyRef::StaticStruct();
+		}
+
+		return false;
+	}
+}
+
+#endif

+ 11 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilitySchema.cpp

@@ -0,0 +1,11 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "EzAbilitySchema.h"
+
+bool UEzAbilitySchema::IsChildOfBlueprintBase(const UClass* InClass) const
+{
+	//return InClass->IsChildOf<UEzAbilityNodeBlueprintBase>();
+	return false;
+}
+

+ 16 - 1
Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityTypes.cpp

@@ -53,6 +53,21 @@ FCompactEzAbilityState::FCompactEzAbilityState()
 {
 }
 
-FCompactEzAbilityTransition::FCompactEzAbilityTransition()
+bool FEzAbilityStateLink::Serialize(FStructuredArchive::FSlot Slot)
 {
+	//Slot.GetUnderlyingArchive().UsingCustomVersion(FEzAbilityCustomVersion::GUID);
+	return false; // Let the default serializer handle serializing.
+}
+
+void FEzAbilityStateLink::PostSerialize(const FArchive& Ar)
+{
+	// #if WITH_EDITORONLY_DATA
+ //    	const int32 CurrentVersion = Ar.CustomVer(FEzAbilityCustomVersion::GUID);
+ //    	if (CurrentVersion < FEzAbilityCustomVersion::AddedExternalTransitions)
+ //    	{
+ //    PRAGMA_DISABLE_DEPRECATION_WARNINGS
+ //    		LinkType = Type_DEPRECATED;
+ //    PRAGMA_ENABLE_DEPRECATION_WARNINGS
+ //    	}
+ //    #endif // 
 }

+ 29 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbility_Replicate.cpp

@@ -5,6 +5,16 @@ bool UEzAbility::ShouldReplicate(const FEzAbilityInstance& Instance) const
 	return true;
 }
 
+const FCompactEzAbilityState* UEzAbility::GetStateFromHandle(const FEzAbilityStateHandle StateHandle) const
+{
+	return States.IsValidIndex(StateHandle.Index) ? &States[StateHandle.Index] : nullptr;
+}
+
+const FCompactEzAbilityTransition* UEzAbility::GetTransitionFromIndex(const FEzAbilityIndex16 TransitionIndex) const
+{
+	return TransitionIndex.IsValid() && Transitions.IsValidIndex(TransitionIndex.Get()) ? &Transitions[TransitionIndex.Get()] : nullptr;
+}
+
 TSharedPtr<FEzAbilityInstanceData> UEzAbility::GetSharedInstanceData() const
 {
 	// Create a unique index for each thread.
@@ -44,3 +54,22 @@ TSharedPtr<FEzAbilityInstanceData> UEzAbility::GetSharedInstanceData() const
 
 	return PerThreadSharedInstanceData[ThreadIndex];
 }
+
+FConstStructView UEzAbility::GetNode(const int32 NodeIndex) const
+{
+	return Nodes.IsValidIndex(NodeIndex) ? Nodes[NodeIndex] : FConstStructView();
+}
+
+FEzAbilityIndex16 UEzAbility::GetNodeIndexFromId(const FGuid Id) const
+{
+	const FEzAbilityNodeIdToIndex* Entry = IDToNodeMappings.FindByPredicate([Id](const FEzAbilityNodeIdToIndex& Entry){ return Entry.Id == Id; });
+	return Entry != nullptr ? Entry->Index : FEzAbilityIndex16::Invalid;
+}
+
+FGuid UEzAbility::GetNodeIdFromIndex(const FEzAbilityIndex16 NodeIndex) const
+{
+	const FEzAbilityNodeIdToIndex* Entry = NodeIndex.IsValid()
+			? IDToNodeMappings.FindByPredicate([NodeIndex](const FEzAbilityNodeIdToIndex& Entry){ return Entry.Index == NodeIndex; })
+			: nullptr;
+	return Entry != nullptr ? Entry->Id : FGuid();
+}

+ 311 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/Debugger/EzAbilityDebuggerTypes.h

@@ -0,0 +1,311 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "EzAbilityTraceTypes.h"
+
+#if WITH_EZABILITY_DEBUGGER
+
+#include "Math/Range.h"
+#include "EzAbility.h"
+#include "EzAbilityTypes.h"
+#include "TraceServices/Model/Frames.h"
+
+#endif
+
+UENUM()
+enum class EEzAbilityBreakpointType : uint8
+{
+	Unset,
+	OnEnter,
+	OnExit,
+	OnTransition,
+};
+
+#if WITH_EZABILITY_DEBUGGER
+
+class UEzAbility;
+enum class EEzAbilityTraceEventType : uint8;
+
+namespace UE::EzAbilityDebugger
+{
+/**
+ * Struct indicating the index of the first event for a given trace recording frame.
+ */
+struct FFrameSpan
+{
+	FFrameSpan() = default;
+	FFrameSpan(const TraceServices::FFrame& Frame, const double RecordingWorldTime, const int32 EventIdx)
+		: Frame(Frame)
+		, WorldTime(RecordingWorldTime)
+		, EventIdx(EventIdx)
+	{
+	}
+
+	double GetWorldTimeStart() const { return WorldTime; }
+	double GetWorldTimeEnd() const { return WorldTime + (Frame.EndTime - Frame.StartTime); }
+
+	/** Frame index in the analysis session */
+	TraceServices::FFrame Frame;
+
+	/** World simulation time associated to that Frame index */
+	double WorldTime = 0;
+
+	/** Index of the first event for that Frame index */
+	int32 EventIdx = INDEX_NONE;
+};
+
+
+/**
+ * Struct describing a state tree instance for a given EzAbility asset
+ */
+struct EZABILITY_API FInstanceDescriptor
+{
+	FInstanceDescriptor() = default;
+	FInstanceDescriptor(const UEzAbility* InEzAbility, const FEzAbilityInstanceDebugId InId, const FString& InName, const TRange<double> InLifetime);
+
+	bool IsValid() const;
+
+	bool operator==(const FInstanceDescriptor& Other) const
+	{
+		return EzAbility == Other.EzAbility && Id == Other.Id;
+	}
+
+	bool operator!=(const FInstanceDescriptor& Other) const
+	{
+		return !(*this == Other);
+	}
+
+	friend FString LexToString(const FInstanceDescriptor& InstanceDesc)
+	{
+		return FString::Printf(TEXT("%s | %s | %s"),
+			*GetNameSafe(InstanceDesc.EzAbility.Get()),
+			*LexToString(InstanceDesc.Id),
+			*InstanceDesc.Name);
+	}
+
+	friend uint32 GetTypeHash(const FInstanceDescriptor& Desc)
+	{
+		return GetTypeHash(Desc.Id);
+	}
+
+	TRange<double> Lifetime = TRange<double>(0);
+	TWeakObjectPtr<const UEzAbility> EzAbility = nullptr;
+	FString Name;
+	FEzAbilityInstanceDebugId Id = FEzAbilityInstanceDebugId::Invalid;
+};
+
+
+/**
+ * Struct holding organized events associated to a given state tree instance.
+ */
+struct EZABILITY_API FInstanceEventCollection
+{
+	FInstanceEventCollection() = default;
+	explicit FInstanceEventCollection(const FEzAbilityInstanceDebugId& InstanceId)
+		: InstanceId(InstanceId)
+	{
+	}
+
+	friend bool operator==(const FInstanceEventCollection& Lhs, const FInstanceEventCollection& RHS)
+	{
+		return Lhs.InstanceId == RHS.InstanceId;
+	}
+
+	friend bool operator!=(const FInstanceEventCollection& Lhs, const FInstanceEventCollection& RHS)
+	{
+		return !(Lhs == RHS);
+	}
+
+	bool IsValid() const { return InstanceId.IsValid(); }
+	bool IsInvalid() const { return !IsValid(); }
+
+	struct FActiveStatesChangePair
+	{
+		FActiveStatesChangePair(const int32 SpanIndex, const int32 EventIndex)
+			: SpanIndex(SpanIndex),
+			  EventIndex(EventIndex)
+		{
+		}
+
+		int32 SpanIndex = INDEX_NONE;
+		int32 EventIndex = INDEX_NONE;
+	};
+
+	/** Id of the instance associated to the stored events. */
+	FEzAbilityInstanceDebugId InstanceId;
+
+	/** All events received for this instance. */
+	TArray<FEzAbilityTraceEventVariantType> Events;
+
+	/** Spans for frames with events. Each span contains the frame information and the index of the first event for that frame. */
+	TArray<FFrameSpan> FrameSpans;
+
+	/** Indices of span and event for frames with a change of activate states. */
+	TArray<FActiveStatesChangePair> ActiveStatesChanges;
+
+	/**
+	 * Returns the event collection associated to the currently selected instance.
+	 * An invalid empty collection is returned if there is no selected instance. (IsValid needs to be called).
+	 * @return Event collection associated to the selected instance or an invalid one if not found.
+	 */
+	static const FInstanceEventCollection Invalid;
+};
+
+struct EZABILITY_API FScrubState
+{
+	explicit FScrubState(const TArray<FInstanceEventCollection>& EventCollections)
+		: EventCollections(EventCollections)
+	{
+	}
+
+	/** @return Index of the currently selected event collection; INDEX_NONE if nothing is selected. */
+	int32 GetEventCollectionIndex() const { return EventCollectionIndex; }
+
+	/** Assigns a new collection index and updates internal indices for current scrub time. */
+	void SetEventCollectionIndex(const int32 InEventCollectionIndex);
+
+	/** @return Index of the span for the currently selected frame; INDEX_NONE if there is no span for the current scrub time. */
+	int32 GetFrameSpanIndex() const { return FrameSpanIndex; }
+
+	/** @return Index of the list of active states for the currently selected frame; INDEX_NONE if there is no active states for the current scrub time. */
+	int32 GetActiveStatesIndex() const { return ActiveStatesIndex; }
+
+	/** @return Current scrub time. */
+	double GetScrubTime() const { return ScrubTime; }
+
+	/**
+	 * Updates internal indices based on the new time.
+	 * @return true if values were updated; false otherwise (i.e. no changes)
+	 */
+	bool SetScrubTime(double NewScrubTime);
+
+	/**
+	 * Indicates if the current scrub state points to a valid frame.
+	 * @return True if the frame index is set
+	 */
+	bool IsInBounds() const { return ScrubTimeBoundState == EScrubTimeBoundState::InBounds; }
+
+	/**
+	 * Indicates if the current scrub state points to an active states entry in the event collection.
+	 * @return True if the collection and active states indices are set
+	 */
+	bool IsPointingToValidActiveStates() const { return EventCollectionIndex != INDEX_NONE && ActiveStatesIndex != INDEX_NONE; }
+
+	/** Indicates if there is a frame before with events. */
+	bool HasPreviousFrame() const;
+	
+	/**
+	 * Set scrubbing info using the previous frame with events.
+	 * HasPreviousFrame must be used to validate that this method can be called otherwise some checks might fail.
+	 * @return Adjusted scrub time
+	 */
+	double GotoPreviousFrame();
+	
+	/** Indicates if there is a frame after with events. */
+	bool HasNextFrame() const;
+	
+	/**
+	 * Set scrubbing info using the next frame with events.
+	 * HasPreviousFrame must be used to validate that this method can be called otherwise some checks might fail.
+	 * @return Adjusted scrub time
+	 */
+	double GotoNextFrame();
+	
+	/** Indicates if there is a frame before where the EzAbility has a different list of active states. */
+	bool HasPreviousActiveStates() const;
+	
+	/**
+	 * Set scrubbing info using the previous frame where the EzAbility has a different list of active states.
+	 * HasPreviousActiveStates must be used to validate that this method can be called otherwise some checks might fail.
+	 * @return Adjusted scrub time
+	 */
+	double GotoPreviousActiveStates();
+	
+	/** Indicates if there is a frame after where the EzAbility has a different list of active states. */
+	bool HasNextActiveStates() const;
+	
+	/**
+	 * Set scrubbing info using the next frame where the EzAbility has a different list of active states.
+	 * HasNextActiveStates must be used to validate that this method can be called otherwise some checks might fail.
+	 * @return Adjusted scrub time
+	 */
+	double GotoNextActiveStates();
+
+	/**
+	 * Returns the event collection associated to the selected instance.
+	 * An invalid empty collection is returned if there is no selected instance (IsValid needs to be called).
+	 * @return Event collection associated to the selected instance or an invalid one if not found.
+	 */
+	const FInstanceEventCollection& GetEventCollection() const;
+
+private:
+	enum class EScrubTimeBoundState : uint8
+	{
+		Unset,
+		/** There are events but current time is before the first frame. */
+		BeforeLowerBound,
+		/** There are events and current time is within the frames received. */
+		InBounds,
+		/** There are events but current time is after the last frame. */
+		AfterHigherBound
+	};
+
+	void SetFrameSpanIndex(int32 NewFrameSpanIndex);
+	void SetActiveStatesIndex(int32 NewActiveStatesIndex);
+	void UpdateActiveStatesIndex(int32 SpanIndex);
+
+	const TArray<FInstanceEventCollection>& EventCollections;
+	double ScrubTime = 0;
+	int32 EventCollectionIndex = INDEX_NONE;
+	uint64 TraceFrameIndex = INDEX_NONE;
+	int32 FrameSpanIndex = INDEX_NONE;
+	int32 ActiveStatesIndex = INDEX_NONE;
+	EScrubTimeBoundState ScrubTimeBoundState = EScrubTimeBoundState::Unset;
+};
+
+} // UE::EzAbilityDebugger
+
+struct FEzAbilityDebuggerBreakpoint
+{
+	// Wrapper structs to be able to use TVariant with more than one type based on FEzAbilityIndex16 (can't use 'using')
+	struct FEzAbilityTaskIndex
+	{
+		FEzAbilityTaskIndex() = default;
+		explicit FEzAbilityTaskIndex(const FEzAbilityIndex16& Index)
+			: Index(Index)
+		{
+		}
+
+		FEzAbilityIndex16 Index;
+	};
+	
+	struct FEzAbilityTransitionIndex
+	{
+		FEzAbilityTransitionIndex() = default;
+		explicit FEzAbilityTransitionIndex(const FEzAbilityIndex16& Index)
+			: Index(Index)
+		{
+		}
+		FEzAbilityIndex16 Index;
+	};
+
+	using FIdentifierVariantType = TVariant<FEzAbilityStateHandle, FEzAbilityTaskIndex, FEzAbilityTransitionIndex>;
+	
+	FEzAbilityDebuggerBreakpoint();
+	explicit FEzAbilityDebuggerBreakpoint(const FEzAbilityStateHandle StateHandle, const EEzAbilityBreakpointType BreakpointType);
+	explicit FEzAbilityDebuggerBreakpoint(const FEzAbilityTaskIndex Index, const EEzAbilityBreakpointType BreakpointType);
+	explicit FEzAbilityDebuggerBreakpoint(const FEzAbilityTransitionIndex Index, const EEzAbilityBreakpointType BreakpointType);
+
+	EZABILITY_API bool IsMatchingEvent(const FEzAbilityTraceEventVariantType& Event) const;
+
+	FIdentifierVariantType ElementIdentifier;
+	EEzAbilityBreakpointType BreakpointType;
+	EEzAbilityTraceEventType EventType;
+
+private:
+	static EEzAbilityTraceEventType GetMatchingEventType(EEzAbilityBreakpointType BreakpointType);
+};
+
+#endif // WITH_EZABILITY_DEBUGGER
+

+ 257 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/Debugger/EzAbilityTraceTypes.h

@@ -0,0 +1,257 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "Misc/TVariant.h"
+#include "EzAbilityIndexTypes.h"
+#include "EzAbilityExecutionTypes.h"
+#include "EzAbilityTraceTypes.generated.h"
+
+class UEzAbility;
+struct FEzAbilityStateHandle;
+enum class EEzAbilityStateSelectionBehavior : uint8;
+enum class EAbilityRunStatus : uint8;
+
+
+UENUM()
+enum class EEzAbilityTraceEventType : uint8
+{
+	Unset,
+	OnEntering				UMETA(DisplayName = "Entering"),
+	OnEntered				UMETA(DisplayName = "Entered"),
+	OnExiting				UMETA(DisplayName = "Exiting"),
+	OnExited				UMETA(DisplayName = "Exited"),
+	Push					UMETA(DisplayName = "Push"),
+	Pop						UMETA(DisplayName = "Pop"),
+	OnStateSelected			UMETA(DisplayName = "Selected"),
+	OnStateCompleted		UMETA(DisplayName = "Completed"),
+	OnTicking				UMETA(DisplayName = "Tick"),
+	OnTaskCompleted			UMETA(DisplayName = "Completed"),
+	OnTicked				UMETA(DisplayName = "Ticked"),
+	Passed					UMETA(DisplayName = "Passed"),
+	Failed					UMETA(DisplayName = "Failed"),
+	ForcedSuccess			UMETA(DisplayName = "Forced Success"),
+	ForcedFailure			UMETA(DisplayName = "Forced Failure"),
+	InternalForcedFailure	UMETA(DisplayName = "Internal Forced Failure"),
+	OnEvaluating			UMETA(DisplayName = "Evaluating"),
+	OnTransition			UMETA(DisplayName = "Transition"),
+	OnAbilityStarted		UMETA(DisplayName = "Ability Started"),
+	OnAbilityStopped		UMETA(DisplayName = "Ability Stopped")
+
+};
+
+#if WITH_EZABILITY_DEBUGGER
+
+struct FEzAbilityTraceBaseEvent
+{
+	explicit FEzAbilityTraceBaseEvent(const double RecordingWorldTime, const EEzAbilityTraceEventType EventType)
+		: RecordingWorldTime(RecordingWorldTime)
+		, EventType(EventType)
+	{
+	}
+
+	static FString GetDataTypePath() { return TEXT(""); }
+	static FString GetDataAsText() { return TEXT(""); }
+
+	double RecordingWorldTime = 0;
+	EEzAbilityTraceEventType EventType;
+};
+
+struct FEzAbilityTracePhaseEvent : FEzAbilityTraceBaseEvent
+{
+	explicit FEzAbilityTracePhaseEvent(const double RecordingWorldTime, const EEzAbilityUpdatePhase Phase, const EEzAbilityTraceEventType EventType, const FEzAbilityStateHandle StateHandle)
+		: FEzAbilityTraceBaseEvent(RecordingWorldTime, EventType)
+		, Phase(Phase)
+		, StateHandle(StateHandle)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+
+	EEzAbilityUpdatePhase Phase;
+	FEzAbilityStateHandle StateHandle;
+};
+
+struct FEzAbilityTraceLogEvent : FEzAbilityTraceBaseEvent
+{
+	explicit FEzAbilityTraceLogEvent(const double RecordingWorldTime, const FString& Message)
+		: FEzAbilityTraceBaseEvent(RecordingWorldTime, EEzAbilityTraceEventType::Unset)
+		, Message(Message)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+
+	FString Message;
+};
+
+struct FEzAbilityTracePropertyEvent : FEzAbilityTraceLogEvent
+{
+	explicit FEzAbilityTracePropertyEvent(const double RecordingWorldTime, const FString& Message)
+		: FEzAbilityTraceLogEvent(RecordingWorldTime, Message)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+};
+
+struct FEzAbilityTraceTransitionEvent : FEzAbilityTraceBaseEvent
+{
+	explicit FEzAbilityTraceTransitionEvent(const double RecordingWorldTime, const FEzAbilityTransitionSource TransitionSource, const EEzAbilityTraceEventType EventType)
+		: FEzAbilityTraceBaseEvent(RecordingWorldTime, EventType)
+		, TransitionSource(TransitionSource)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+
+	FEzAbilityTransitionSource TransitionSource;
+};
+
+struct FEzAbilityTraceNodeEvent : FEzAbilityTraceBaseEvent
+{
+	explicit FEzAbilityTraceNodeEvent(const double RecordingWorldTime, const FEzAbilityIndex16 Index, const EEzAbilityTraceEventType EventType)
+		: FEzAbilityTraceBaseEvent(RecordingWorldTime, EventType)
+		, Index(Index)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+	
+	FEzAbilityIndex16 Index;
+};
+
+struct FEzAbilityTraceStateEvent : FEzAbilityTraceNodeEvent
+{
+	explicit FEzAbilityTraceStateEvent(const double RecordingWorldTime, const FEzAbilityIndex16 Index, const EEzAbilityTraceEventType EventType)
+		: FEzAbilityTraceNodeEvent(RecordingWorldTime, Index, EventType)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FEzAbilityStateHandle GetStateHandle() const;
+};
+
+struct FEzAbilityTraceTaskEvent : FEzAbilityTraceNodeEvent
+{
+	explicit FEzAbilityTraceTaskEvent(const double RecordingWorldTime, const FEzAbilityIndex16 Index, const EEzAbilityTraceEventType EventType, const EAbilityRunStatus Status, const FString& TypePath, const FString& InstanceDataAsText)
+		: FEzAbilityTraceNodeEvent(RecordingWorldTime, Index, EventType)
+		, TypePath(TypePath)
+		, InstanceDataAsText(InstanceDataAsText)
+		, Status(Status)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+
+	FString GetDataTypePath() const { return TypePath; }
+	FString GetDataAsText() const { return InstanceDataAsText; }
+
+	FString TypePath;
+	FString InstanceDataAsText;
+	EAbilityRunStatus Status;
+};
+
+struct FEzAbilityTraceEvaluatorEvent : FEzAbilityTraceNodeEvent
+{
+	explicit FEzAbilityTraceEvaluatorEvent(const double RecordingWorldTime, const FEzAbilityIndex16 Index, const EEzAbilityTraceEventType EventType, const FString& TypePath, const FString& InstanceDataAsText)
+		: FEzAbilityTraceNodeEvent(RecordingWorldTime, Index, EventType)
+		, TypePath(TypePath)
+		, InstanceDataAsText(InstanceDataAsText)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+
+	FString GetDataTypePath() const { return TypePath; }
+	FString GetDataAsText() const { return InstanceDataAsText; }
+
+	FString TypePath;
+	FString InstanceDataAsText;
+};
+
+struct FEzAbilityTraceConditionEvent : FEzAbilityTraceNodeEvent
+{
+	explicit FEzAbilityTraceConditionEvent(const double RecordingWorldTime, const FEzAbilityIndex16 Index, const EEzAbilityTraceEventType EventType, const FString& TypePath, const FString& InstanceDataAsText)
+		: FEzAbilityTraceNodeEvent(RecordingWorldTime, Index, EventType)
+		, TypePath(TypePath)
+		, InstanceDataAsText(InstanceDataAsText)
+	{
+	}
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+
+	FString GetDataTypePath() const { return TypePath; }
+	FString GetDataAsText() const { return InstanceDataAsText; }
+
+	FString TypePath;
+	FString InstanceDataAsText;
+};
+
+struct FEzAbilityTraceActiveStates
+{
+	struct FAssetActiveStates
+	{
+		TWeakObjectPtr<const UEzAbility> WeakEzAbility;
+		TArray<FEzAbilityStateHandle> ActiveStates;
+	};
+
+	TArray<FAssetActiveStates> PerAssetStates;
+};
+
+struct FEzAbilityTraceActiveStatesEvent : FEzAbilityTraceBaseEvent
+{
+	// Intentionally implemented in source file to compile 'TArray<FEzAbilityStateHandle>' using only forward declaration.
+	EZABILITY_API explicit FEzAbilityTraceActiveStatesEvent(const double RecordingWorldTime);
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+
+	FEzAbilityTraceActiveStates ActiveStates;
+};
+
+struct FEzAbilityTraceInstanceFrameEvent : FEzAbilityTraceBaseEvent
+{
+	explicit FEzAbilityTraceInstanceFrameEvent(const double RecordingWorldTime, const EEzAbilityTraceEventType EventType, const UEzAbility* EzAbility);
+
+	EZABILITY_API FString ToFullString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetValueString(const UEzAbility& EzAbility) const;
+	EZABILITY_API FString GetTypeString(const UEzAbility& EzAbility) const;
+
+	TWeakObjectPtr<const UEzAbility> WeakEzAbility;
+};
+
+/** Type aliases for EzAbility trace events */
+using FEzAbilityTraceEventVariantType = TVariant<FEzAbilityTracePhaseEvent,
+												FEzAbilityTraceLogEvent,
+												FEzAbilityTracePropertyEvent,
+												FEzAbilityTraceNodeEvent,
+												FEzAbilityTraceStateEvent,
+												FEzAbilityTraceTaskEvent,
+												FEzAbilityTraceEvaluatorEvent,
+												FEzAbilityTraceTransitionEvent,
+												FEzAbilityTraceConditionEvent,
+												FEzAbilityTraceActiveStatesEvent,
+												FEzAbilityTraceInstanceFrameEvent>;
+
+#endif // WITH_EZABILITY_DEBUGGER
+

+ 28 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbility.h

@@ -4,6 +4,7 @@
 
 #include "CoreMinimal.h"
 #include "EzAbilityInstanceData.h"
+#include "EzAbilityPropertyBindings.h"
 #include "InstancedStructContainer.h"
 #include "EzAbilityTypes.h"
 #include "PropertyBag.h"
@@ -23,8 +24,18 @@ public:
 	virtual bool CanActivateAbility(FEzAbilityContext& Context, FText& OutText) const;
 	virtual bool ShouldReplicate(const FEzAbilityInstance& Instance) const;
 
+	const FCompactEzAbilityState* GetStateFromHandle(const FEzAbilityStateHandle StateHandle) const;
+	const FCompactEzAbilityTransition* GetTransitionFromIndex(const FEzAbilityIndex16 TransitionIndex) const;
+	
 	const FInstancedPropertyBag& GetDefaultParameters() const { return Parameters; }
 	TSharedPtr<FEzAbilityInstanceData> GetSharedInstanceData() const;
+
+	const FEzAbilityPropertyBindings& GetPropertyBindings() const { return PropertyBindings; }
+
+	FConstStructView GetNode(const int32 NodeIndex) const;
+	const FInstancedStructContainer& GetNodes() const { return Nodes; }
+	FEzAbilityIndex16 GetNodeIndexFromId(const FGuid Id) const;
+	FGuid GetNodeIdFromIndex(const FEzAbilityIndex16 NodeIndex) const;
 protected:
 	UFUNCTION(BlueprintNativeEvent)
 	bool K2_ActivateAbility(FEzAbilityContext& Context) const;
@@ -76,9 +87,26 @@ public:
 	UPROPERTY()
 	FEzAbilityDataHandle	ParameterDataHandle		= FEzAbilityDataHandle::Invalid;
 
+	UPROPERTY()
+	FEzAbilityPropertyBindings PropertyBindings;
+	
 	mutable FRWLock PerThreadSharedInstanceDataLock;
 	mutable TArray<TSharedPtr<FEzAbilityInstanceData>> PerThreadSharedInstanceData;
 
 	UPROPERTY()
 	uint32 LastCompiledEditorDataHash = 0;
+
+	UPROPERTY()
+	TArray<FEzAbilityNodeIdToIndex> IDToNodeMappings;
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	///Editor
+#if WITH_EDITORONLY_DATA
+	/** Edit time data for the StateTree, instance of UEzAbilityEditorData */
+	UPROPERTY()
+	TObjectPtr<UObject> EditorData;
+
+	FDelegateHandle OnObjectsReinstancedHandle;
+	FDelegateHandle OnUserDefinedStructReinstancedHandle;
+#endif
 };

+ 15 - 2
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityContext.h

@@ -11,6 +11,9 @@
 class UEzAbilityComponent;
 class UEzAbility;
 
+DECLARE_DELEGATE_RetVal_FourParams(bool, FOnCollectEzAbilityExternalData, const FEzAbilityContext& /*Context*/, const UEzAbility* /*Ability*/, TArrayView<const FEzAbilityExternalDataDesc> /*ExternalDataDescs*/, TArrayView<FEzAbilityDataView> /*OutDataViews*/);
+
+
 USTRUCT(BlueprintType)
 struct FEzAbilityTarget
 {
@@ -45,6 +48,10 @@ struct EZABILITY_API FEzAbilityContext
 {
 	GENERATED_BODY()
 public:
+	FEzAbilityContext() = default;
+	FEzAbilityContext(UObject& InOwner, const UEzAbility& InAbility, FEzAbilityInstanceData& InInstanceData, const FOnCollectEzAbilityExternalData& CollectExternalDataCallback = {});
+	~FEzAbilityContext();
+	
 	void InitContext(UEzAbilityComponent* Component);
 	bool IsLocallyControlled() const;
 	bool IsNetAuthority() const;
@@ -63,6 +70,14 @@ public:
 	EAbilityRunStatus Start(UEzAbility* InAbility, const struct FEzAbilityParameter& Parameter, FText& OutText);
 	EAbilityRunStatus GetAbilityRunStatus() const;
 
+	EAbilityRunStatus Tick(float DeltaTime);
+
+	const FEzAbilityExecutionFrame* GetCurrentlyProcessedFrame()		const { return CurrentlyProcessedFrame; }
+	const FEzAbilityExecutionFrame* GetCurrentlyProcessedParentFrame()	const { return CurrentlyProcessedParentFrame; }
+	
+	static FEzAbilityDataView GetDataView(FEzAbilityInstanceStorage& InstanceDataStorage, FEzAbilityInstanceStorage* CurrentlyProcessedSharedInstanceStorage, const FEzAbilityExecutionFrame* ParentFrame, const FEzAbilityExecutionFrame& CurrentFrame, TConstArrayView<FEzAbilityDataView> ContextAndExternalDataViews, const FEzAbilityDataHandle Handle);
+
+	TArray<FName> GetActiveStateNames() const;
 protected:
 	void Reset();
 	FString GetInstanceDescription() const;
@@ -111,8 +126,6 @@ public:
 	const FEzAbilityExecutionFrame* CurrentlyProcessedParentFrame = nullptr; 
 	const FEzAbilityExecutionFrame* CurrentlyProcessedFrame = nullptr;
 	FEzAbilityInstanceStorage* CurrentlyProcessedSharedInstanceStorage = nullptr;
-
-
 	
 	/** Helper struct to set current node data. */
 	struct FNodeInstanceDataScope

+ 74 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityDelegates.h

@@ -0,0 +1,74 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "Delegates/Delegate.h"
+
+class UEzAbility;
+
+namespace UE::EzAbility::Delegates
+{
+
+#if WITH_EDITOR
+
+/** Called when linkable name in a EzAbility has changed. */
+DECLARE_MULTICAST_DELEGATE_OneParam(FOnIdentifierChanged, const UEzAbility& /*EzAbility*/);
+extern EZABILITY_API FOnIdentifierChanged OnIdentifierChanged;
+
+/**
+ * Called when schema of the EzAbility EditorData has changed.
+ * This is used to refresh the asset editor.
+ * Note that this is NOT called when updating the EzAbility schema from the EditorData on successful compilation.
+ */
+DECLARE_MULTICAST_DELEGATE_OneParam(FOnSchemaChanged, const UEzAbility& /*EzAbility*/);
+extern EZABILITY_API FOnSchemaChanged OnSchemaChanged;
+
+/**
+ * Called when parameters of the EzAbility EditorData changed.
+ * This should mainly used by the asset editor to maintain consistency in the UI for manipulations on the EditorData
+ * until the tree gets compiled.
+ */
+DECLARE_MULTICAST_DELEGATE_OneParam(FOnParametersChanged, const UEzAbility& /*EzAbility*/);
+extern EZABILITY_API FOnParametersChanged OnParametersChanged;
+
+/**
+ * Called when parameters of a EzAbility State changed.
+ * This should mainly used by the asset editor to maintain consistency in the UI for manipulations.
+ */
+DECLARE_MULTICAST_DELEGATE_TwoParams(FOnStateParametersChanged, const UEzAbility& /*EzAbility*/, const FGuid /*StateID*/);
+extern EZABILITY_API FOnStateParametersChanged OnStateParametersChanged;
+
+/**
+ * Called when Global Tasks or Evaluators of the EzAbility EditorData changed.
+ * This should mainly used by the asset editor to maintain consistency in the UI for manipulations on the EditorData.
+ */
+DECLARE_MULTICAST_DELEGATE_OneParam(FOnGlobalDataChanged, const UEzAbility& /*EzAbility*/);
+extern EZABILITY_API FOnGlobalDataChanged OnGlobalDataChanged;
+
+/**
+ * Called when breakpoints of the EzAbility EditorData changed.
+ * This should mainly used by the asset editor to update the debugger.
+ */
+DECLARE_MULTICAST_DELEGATE_OneParam(FOnBreakpointsChanged, const UEzAbility& /*EzAbility*/);
+extern EZABILITY_API FOnBreakpointsChanged OnBreakpointsChanged;
+
+/** Called when compilation succeeds */
+DECLARE_MULTICAST_DELEGATE_OneParam(FOnPostCompile, const UEzAbility& /*EzAbility*/);
+extern EZABILITY_API FOnPostCompile OnPostCompile;
+
+/** Request EzAbility compilation. Works only in editor. */
+DECLARE_DELEGATE_RetVal_OneParam(bool, FOnRequestCompile, UEzAbility& /*EzAbilityToCompile*/);
+extern EZABILITY_API FOnRequestCompile OnRequestCompile;
+
+#endif // WITH_EDITOR
+
+#if WITH_EZABILITY_DEBUGGER
+
+/** Called by the EzAbility module when EzAbility traces are enabled/disabled. */
+DECLARE_MULTICAST_DELEGATE_OneParam(FOnTracingStateChanged, bool /*bTracesEnabled*/);
+extern EZABILITY_API FOnTracingStateChanged OnTracingStateChanged;
+
+#endif // WITH_EZABILITY_DEBUGGER
+
+}; // UE::EzAbility::Delegates
+

+ 105 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityExecutionTypes.h

@@ -502,3 +502,108 @@ PRAGMA_ENABLE_DEPRECATION_WARNINGS
 	
 };
 
+/**
+ * Handle to access an external struct or object.
+ * Note: Use the templated version below. 
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityExternalDataHandle
+{
+	GENERATED_BODY()
+
+	PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	FEzAbilityExternalDataHandle() = default;
+	FEzAbilityExternalDataHandle(const FEzAbilityExternalDataHandle& Other) = default;
+	FEzAbilityExternalDataHandle(FEzAbilityExternalDataHandle&& Other) = default;
+	FEzAbilityExternalDataHandle& operator=(FEzAbilityExternalDataHandle const& Other) = default;
+	FEzAbilityExternalDataHandle& operator=(FEzAbilityExternalDataHandle&& Other) = default;
+	PRAGMA_ENABLE_DEPRECATION_WARNINGS
+
+	static const FEzAbilityExternalDataHandle Invalid;
+	
+	bool IsValid() const { return DataHandle.IsValid(); }
+
+	UE_DEPRECATED(5.4, "Index is deprecated, use DataHandle instead.")
+	static bool IsValidIndex(const int32 Index) { return FEzAbilityDataHandle::IsValidIndex(Index); }
+
+	UPROPERTY()
+	FEzAbilityDataHandle DataHandle = FEzAbilityDataHandle::Invalid;
+
+#if WITH_EDITORONLY_DATA
+	UE_DEPRECATED(5.4, "Use DataHandle instead.")
+	UPROPERTY()
+	FEzAbilityIndex16 DataViewIndex_DEPRECATED = FEzAbilityIndex16::Invalid;
+#endif // WITH_EDITORONLY_DATA
+};
+
+/**
+ * Describes an external data. The data can point to a struct or object.
+ * The code that handles EzAbility ticking is responsible for passing in the actually data, see FEzAbilityExecutionContext.
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityExternalDataDesc
+{
+	GENERATED_BODY()
+
+	FEzAbilityExternalDataDesc() = default;
+	FEzAbilityExternalDataDesc(const UStruct* InStruct, const EEzAbilityExternalDataRequirement InRequirement) : Struct(InStruct), Requirement(InRequirement) {}
+
+	FEzAbilityExternalDataDesc(const FName InName, const UStruct* InStruct, const FGuid InGuid)
+		: Struct(InStruct)
+		, Name(InName)
+#if WITH_EDITORONLY_DATA
+		, ID(InGuid)
+#endif
+	{}
+
+	/** @return true if the DataView is compatible with the descriptor. */
+	bool IsCompatibleWith(const FEzAbilityDataView& DataView) const
+	{
+		if (DataView.GetStruct()->IsChildOf(Struct))
+		{
+			return true;
+		}
+		
+		if (const UClass* DataDescClass = Cast<UClass>(Struct))
+		{
+			if (const UClass* DataViewClass = Cast<UClass>(DataView.GetStruct()))
+			{
+				return DataViewClass->ImplementsInterface(DataDescClass);
+			}
+		}
+		
+		return false;
+	}
+	
+	bool operator==(const FEzAbilityExternalDataDesc& Other) const
+	{
+		return Struct == Other.Struct && Requirement == Other.Requirement;
+	}
+	
+	/** Class or struct of the external data. */
+	UPROPERTY();
+	TObjectPtr<const UStruct> Struct = nullptr;
+
+	/**
+	 * Name of the external data. Used only for bindable external data (enforced by the schema).
+	 * External data linked explicitly by the nodes (i.e. LinkExternalData) are identified only
+	 * by their type since they are used for unique instance of a given type.  
+	 */
+	UPROPERTY(VisibleAnywhere, Category = Common)
+	FName Name;
+	
+	/** Handle/Index to the EzAbilityExecutionContext data views array */
+	UPROPERTY();
+	FEzAbilityExternalDataHandle Handle;
+
+	/** Describes if the data is required or not. */
+	UPROPERTY();
+	EEzAbilityExternalDataRequirement Requirement = EEzAbilityExternalDataRequirement::Required;
+
+#if WITH_EDITORONLY_DATA
+	/** Unique identifier. Used only for bindable external data. */
+	UPROPERTY()
+	FGuid ID;
+#endif
+};
+

+ 30 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityNodeBase.h

@@ -4,6 +4,7 @@
 
 #include "CoreMinimal.h"
 #include "EzAbilityIndexTypes.h"
+#include "EzAbilityPropertyBindings.h"
 #include "EzAbilityTypes.h"
 #include "UObject/Object.h"
 #include "EzAbilityNodeBase.generated.h"
@@ -29,6 +30,35 @@ PRAGMA_ENABLE_DEPRECATION_WARNINGS
 	virtual UStruct* GetInstanceDataType() const { return nullptr; }
 	virtual bool Link() { return false; }
 
+#if WITH_EDITOR
+	PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	UE_DEPRECATED(5.3, "Use version with FEzAbilityPropertyPath instead.")
+	virtual void OnBindingChanged(const FGuid& ID, FEzAbilityDataView InstanceData, const FEzAbilityEditorPropertyPath& SourcePath, const FEzAbilityEditorPropertyPath& TargetPath, const IEzAbilityBindingLookup& BindingLookup) final {}
+	PRAGMA_ENABLE_DEPRECATION_WARNINGS
+	/**
+	 * Called when binding of any of the properties in the node changes.
+	 * @param ID ID of the item, can be used make property paths to this item.
+	 * @param InstanceData view to the instance data, can be struct or class.
+	 * @param SourcePath Source path of the new binding.
+	 * @param TargetPath Target path of the new binding (the property in the condition).
+	 * @param BindingLookup Reference to binding lookup which can be used to reason about property paths.
+	 */
+	virtual void OnBindingChanged(const FGuid& ID, FEzAbilityDataView InstanceData, const FEzAbilityPropertyPath& SourcePath, const FEzAbilityPropertyPath& TargetPath, const IEzAbilityBindingLookup& BindingLookup) {}
+
+	/**
+	 * Called when a property of the node has been modified externally
+	 * @param PropertyChangedEvent The event for the changed property. PropertyChain's active properties are set relative to node.
+	 * @param InstanceData view to the instance data, can be struct or class.
+	 */
+	virtual void PostEditNodeChangeChainProperty(const FPropertyChangedChainEvent& PropertyChangedEvent, FEzAbilityDataView InstanceDataView) {}
+
+	/**
+	 * Called when a property of node's instance data has been modified externally
+	 * @param PropertyChangedEvent The event for the changed property. PropertyChain's active properties are set relative to instance data.
+	 * @param InstanceData view to the instance data, can be struct or class.
+	 */
+	virtual void PostEditInstanceDataChangeChainProperty(const FPropertyChangedChainEvent& PropertyChangedEvent, FEzAbilityDataView InstanceDataView) {}
+#endif
 
 	UPROPERTY(EditDefaultsOnly, Category = "", meta=(EditCondition = "false", EditConditionHides))
 	FName Name;

+ 1069 - 5
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityPropertyBindings.h

@@ -2,15 +2,1079 @@
 
 #pragma once
 
-#include "CoreMinimal.h"
-#include "UObject/Object.h"
+#include "EzAbilityTypes.h"
+#include "EzAbilityPropertyRefHelpers.h"
 #include "EzAbilityPropertyBindings.generated.h"
 
+class FProperty;
+struct FEzAbilityPropertyPath;
+struct FEzAbilityPropertyBindingCompiler;
+struct FEzAbilityPropertyRef;
+class UEzAbility;
+
+UENUM()
+enum class EEzAbilityBindableStructSource : uint8
+{
+	/** Source is EzAbility context object */
+	Context,
+	/** Source is EzAbility parameter */
+	Parameter,
+	/** Source is EzAbility evaluator */
+	Evaluator,
+	/** Source is EzAbility global task */
+	GlobalTask,
+	/** Source is State parameter */
+	State,
+	/** Source is State task */
+	Task,
+	/** Source is State condition */
+	Condition,
+};
+
+
+/**
+ * The type of access a property path describes.
+ */
+UENUM()
+enum class EEzAbilityPropertyAccessType : uint8
+{
+	Offset,			// Access node is a simple basePtr + offset
+	Object,			// Access node needs to dereference an object at its current address
+	WeakObject,		// Access is a weak object
+	SoftObject,		// Access is a soft object
+	ObjectInstance,	// Access node needs to dereference an object of specific type at its current address
+	StructInstance,	// Access node needs to dereference an instanced struct of specific type at its current address
+	IndexArray,		// Access node indexes a dynamic array
+};
+
+/**
+ * Describes how the copy should be performed.
+ */
+UENUM()
+enum class EEzAbilityPropertyCopyType : uint8
+{
+	None,						// No copying
+	
+	CopyPlain,					// For plain old data types, we do a simple memcpy.
+	CopyComplex,				// For more complex data types, we need to call the properties copy function
+	CopyBool,					// Read and write properties using bool property helpers, as source/dest could be bitfield or boolean
+	CopyStruct,					// Use struct copy operation, as this needs to correctly handle CPP struct ops
+	CopyObject,					// Read and write properties using object property helpers, as source/dest could be regular/weak/soft etc.
+	CopyName,					// FName needs special case because its size changes between editor/compiler and runtime.
+	CopyFixedArray,				// Array needs special handling for fixed size TArrays
+
+	StructReference,			// Copies pointer to a source struct into a FEzAbilityStructRef.
+
+	/* Promote the type during the copy */
+
+	/* Bool promotions */
+	PromoteBoolToByte,
+	PromoteBoolToInt32,
+	PromoteBoolToUInt32,
+	PromoteBoolToInt64,
+	PromoteBoolToFloat,
+	PromoteBoolToDouble,
+
+	/* Byte promotions */
+	PromoteByteToInt32,
+	PromoteByteToUInt32,
+	PromoteByteToInt64,
+	PromoteByteToFloat,
+	PromoteByteToDouble,
+
+	/* Int32 promotions */
+	PromoteInt32ToInt64,
+	PromoteInt32ToFloat,	// This is strictly sketchy because of potential data loss, but it is usually OK in the general case
+	PromoteInt32ToDouble,
+
+	/* UInt32 promotions */
+	PromoteUInt32ToInt64,
+	PromoteUInt32ToFloat,	// This is strictly sketchy because of potential data loss, but it is usually OK in the general case
+	PromoteUInt32ToDouble,
+
+	/* Float promotions */
+	PromoteFloatToInt32,
+	PromoteFloatToInt64,
+	PromoteFloatToDouble,
+
+	/* Double promotions */
+	DemoteDoubleToInt32,
+	DemoteDoubleToInt64,
+	DemoteDoubleToFloat,
+};
+
+/** Enum describing property compatibility */
+enum class EEzAbilityPropertyAccessCompatibility
+{
+	/** Properties are incompatible */
+	Incompatible,
+	/** Properties are directly compatible */
+	Compatible,	
+	/** Properties can be copied with a simple type promotion */
+	Promotable,
+};
+
+/**
+ * Descriptor for a struct or class that can be a binding source or target.
+ * Each struct has unique identifier, which is used to distinguish them, and name that is mostly for debugging and UI.
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityBindableStructDesc
+{
+	GENERATED_BODY()
+
+	FEzAbilityBindableStructDesc() = default;
+
+#if WITH_EDITORONLY_DATA
+	FEzAbilityBindableStructDesc(const FName InName, const UStruct* InStruct, const FEzAbilityDataHandle InDataHandle, const EEzAbilityBindableStructSource InDataSource, const FGuid InGuid)
+		: Struct(InStruct)
+		, Name(InName)
+		, DataHandle(InDataHandle)
+		, DataSource(InDataSource)
+		, ID(InGuid)
+	{
+	}
+
+	UE_DEPRECATED(5.4, "Use constructor with DataHandle instead.")
+	FEzAbilityBindableStructDesc(const FName InName, const UStruct* InStruct, const EEzAbilityBindableStructSource InDataSource, const FGuid InGuid)
+		: Struct(InStruct)
+		, Name(InName)
+		, DataSource(InDataSource)
+		, ID(InGuid)
+	{
+	}
+
+	bool operator==(const FEzAbilityBindableStructDesc& RHS) const
+	{
+		return ID == RHS.ID && Struct == RHS.Struct; // Not checking name, it's cosmetic.
+	}
+#endif
+
+	bool IsValid() const { return Struct != nullptr; }
+
+	FString ToString() const;
+	
+	/** The type of the struct or class. */
+	UPROPERTY()
+	TObjectPtr<const UStruct> Struct = nullptr;
+
+	/** Name of the container (cosmetic). */
+	UPROPERTY()
+	FName Name;
+
+	/** Runtime data the struct represents. */
+	UPROPERTY()
+	FEzAbilityDataHandle DataHandle = FEzAbilityDataHandle::Invalid;
+
+	/** Type of the source. */
+	UPROPERTY()
+	EEzAbilityBindableStructSource DataSource = EEzAbilityBindableStructSource::Context;
+
+#if WITH_EDITORONLY_DATA
+	/** Unique identifier of the struct. */
+	UPROPERTY()
+	FGuid ID;
+#endif
+};
+
+/** Struct describing a path segment in FEzAbilityPropertyPath. */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyPathSegment
+{
+	GENERATED_BODY()
+
+	FEzAbilityPropertyPathSegment() = default;
+
+	explicit FEzAbilityPropertyPathSegment(const FName InName, const int32 InArrayIndex = INDEX_NONE, const UStruct* InInstanceStruct = nullptr)
+		: Name(InName)
+		, ArrayIndex(InArrayIndex)
+		, InstanceStruct(InInstanceStruct)
+	{
+	}
+
+	bool operator==(const FEzAbilityPropertyPathSegment& RHS) const
+	{
+		return Name == RHS.Name && InstanceStruct == RHS.InstanceStruct && ArrayIndex == RHS.ArrayIndex;
+	}
+
+	bool operator!=(const FEzAbilityPropertyPathSegment& RHS) const
+	{
+		return !(*this == RHS);
+	}
+
+	void SetName(const FName InName)
+	{
+		Name = InName;
+	}
+
+	FName GetName() const
+	{
+		return Name;
+	}
+
+	void SetArrayIndex(const int32 InArrayIndex)
+	{
+		ArrayIndex = InArrayIndex;
+	}
+
+	int32 GetArrayIndex() const
+	{
+		return ArrayIndex;
+	}
+
+	void SetInstanceStruct(const UStruct* InInstanceStruct)
+	{
+		InstanceStruct = InInstanceStruct;
+	}
+
+	const UStruct* GetInstanceStruct() const
+	{
+		return InstanceStruct;
+	}
+
+#if WITH_EDITORONLY_DATA
+	FGuid GetPropertyGuid() const
+	{
+		return PropertyGuid;
+	}
+
+	void SetPropertyGuid(const FGuid NewGuid)
+	{
+		PropertyGuid = NewGuid;
+	}
+#endif
+	
+private:
+	/** Name of the property */
+	UPROPERTY()
+	FName Name;
+
+#if WITH_EDITORONLY_DATA
+	/** Guid of the property for Blueprint classes or User Defined Structs. */
+	FGuid PropertyGuid;
+#endif
+
+	/** Array index if the property is dynamic or static array. */
+	UPROPERTY()
+	int32 ArrayIndex = INDEX_NONE;
+
+	/** Type of the instanced struct or object reference by the property at the segment. This allows the path to be resolved when it points to a specific instance. */
+	UPROPERTY()
+	TObjectPtr<const UStruct> InstanceStruct = nullptr;
+};
+
+/**
+ * Struct describing an indirection at specific segment at path.
+ * Returned by FEzAbilityPropertyPath::ResolveIndirections() and FEzAbilityPropertyPath::ResolveIndirectionsWithValue().
+ * Generally there's one indirection per FProperty. Containers have one path segment but two indirection (container + inner type).
+ */
+struct FEzAbilityPropertyPathIndirection
+{
+	FEzAbilityPropertyPathIndirection() = default;
+	explicit FEzAbilityPropertyPathIndirection(const UStruct* InContainerStruct)
+		: ContainerStruct(InContainerStruct)
+	{
+	}
+
+	const FProperty* GetProperty() const { return Property; }
+	const uint8* GetContainerAddress() const { return ContainerAddress; }
+	const UStruct* GetInstanceStruct() const { return InstanceStruct; }
+	const UStruct* GetContainerStruct() const { return ContainerStruct; }
+	int32 GetArrayIndex() const { return ArrayIndex; }
+	int32 GetPropertyOffset() const { return PropertyOffset; }
+	int32 GetPathSegmentIndex() const { return PathSegmentIndex; }
+	EEzAbilityPropertyAccessType GetAccessType() const { return AccessType; }
+	const uint8* GetPropertyAddress() const { return ContainerAddress + PropertyOffset; }
+
+#if WITH_EDITORONLY_DATA
+	FName GetRedirectedName() const { return RedirectedName; }
+	FGuid GetPropertyGuid() const { return PropertyGuid; }
+#endif
+	
+private:
+	/** Property at the indirection. */
+	const FProperty* Property = nullptr;
+	
+	/** Address of the container class/struct where the property belongs to. Only valid if created with ResolveIndirectionsWithValue() */
+	const uint8* ContainerAddress = nullptr;
+	
+	/** Type of the container class/struct. */
+	const UStruct* ContainerStruct = nullptr;
+	
+	/** Type of the instance class/struct of when AccessType is ObjectInstance or StructInstance. */
+	const UStruct* InstanceStruct = nullptr;
+
+#if WITH_EDITORONLY_DATA
+	/** Redirected name, if the give property name was not found but was reconciled using core redirect or property Guid. Requires ResolveIndirections/WithValue() to be called with bHandleRedirects = true. */
+	FName RedirectedName;
+
+	/** Guid of the property for Blueprint classes or User Defined Structs. Requires ResolveIndirections/WithValue() to be called with bHandleRedirects = true. */
+	FGuid PropertyGuid;
+#endif
+	
+	/** Array index for static and dynamic arrays. Note: static array indexing is baked in the PropertyOffset. */
+	int32 ArrayIndex = 0;
+	
+	/** Offset of the property relative to ContainerAddress. Includes static array indexing. */
+	int32 PropertyOffset = 0;
+	
+	/** Index of the path segment where indirection originated from. */
+	int32 PathSegmentIndex = 0;
+	
+	/** How to access the data through the indirection. */
+	EEzAbilityPropertyAccessType AccessType = EEzAbilityPropertyAccessType::Offset;
+
+	friend FEzAbilityPropertyPath;
+};
+	
+
+/**
+ * Representation of a property path used for property binding in EzAbility.
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyPath
+{
+	GENERATED_BODY()
+
+	FEzAbilityPropertyPath() = default;
+
+#if WITH_EDITORONLY_DATA
+	explicit FEzAbilityPropertyPath(const FGuid InStructID)
+		: StructID(InStructID)
+	{
+	}
+
+	explicit FEzAbilityPropertyPath(const FGuid InStructID, const FName PropertyName)
+		: StructID(InStructID)
+	{
+		Segments.Emplace(PropertyName);
+	}
+	
+	explicit FEzAbilityPropertyPath(const FGuid InStructID, TConstArrayView<FEzAbilityPropertyPathSegment> InSegments)
+		: StructID(InStructID)
+		, Segments(InSegments)
+	{
+	}
+#endif // WITH_EDITORONLY_DATA
+	
+	/**
+	 * Parses path from string. The path should be in format: Foo.Bar[1].Baz
+	 * @param InPath Path string to parse
+	 * @return true if path was parsed successfully.
+	 */
+	bool FromString(const FString& InPath);
+
+	/**
+	 * Returns the property path as a one string. Highlight allows to decorate a specific segment.
+	 * @param HighlightedSegment Index of the highlighted path segment
+	 * @param HighlightPrefix String to append before highlighted segment
+	 * @param HighlightPostfix String to append after highlighted segment
+	 * @param bOutputInstances if true, the instance struct types will be output. 
+	 */
+	FString ToString(const int32 HighlightedSegment = INDEX_NONE, const TCHAR* HighlightPrefix = nullptr, const TCHAR* HighlightPostfix = nullptr, const bool bOutputInstances = false) const;
+
+	/**
+	 * Resolves the property path against base struct type. The path is assumed to be relative to the BaseStruct.
+	 * @param BaseStruct Base struct/class type the path is relative to.
+	 * @param OutIndirections Indirections describing how the properties were accessed. 
+	 * @param OutError Optional, pointer to string where error will be logged if update fails.
+	 * @param bHandleRedirects If true, the method will try to resolve missing properties using core redirects, and properties on Blueprint and User Defined Structs by ID. Available only in editor builds!
+	 * @return true of path could be resolved against the base value, and instance types were updated.
+	 */
+	bool ResolveIndirections(const UStruct* BaseStruct, TArray<FEzAbilityPropertyPathIndirection>& OutIndirections, FString* OutError = nullptr, bool bHandleRedirects = false) const;
+	
+	/**
+	 * Resolves the property path against base value. The path is assumed to be relative to the BaseValueView.
+	 * @param BaseValueView Base value the path is relative to.
+	 * @param OutIndirections Indirections describing how the properties were accessed. 
+	 * @param OutError Optional, pointer to string where error will be logged if update fails.
+	 * @param bHandleRedirects If true, the method will try to resolve missing properties using core redirects, and properties on Blueprint and User Defined Structs by ID. Available only in editor builds!
+	 * @return true of path could be resolved against the base value, and instance types were updated.
+	 */
+	bool ResolveIndirectionsWithValue(const FEzAbilityDataView BaseValueView, TArray<FEzAbilityPropertyPathIndirection>& OutIndirections, FString* OutError = nullptr, bool bHandleRedirects = false) const;
+
+	/**
+	 * Updates property segments from base struct type. The path is expected to be relative to the BaseStruct.
+	 * The method handles renamed properties (core redirect, Blueprint and User Defined Structs by ID).
+	 * @param BaseValueView Base value the path is relative to.
+	 * @param OutError Optional, pointer to string where error will be logged if update fails.
+	 * @return true of path could be resolved against the base value, and instance types were updated.
+	 */
+	bool UpdateSegments(const UStruct* BaseStruct, FString* OutError = nullptr);
+
+	/**
+	 * Updates property segments from base value. The path is expected to be relative to the base value.
+	 * The method updates instance types, and handles renamed properties (core redirect, Blueprint and User Defined Structs by ID).
+	 * By storing the instance types on the path, we can resolve the path without the base value later.
+	 * @param BaseValueView Base value the path is relative to.
+	 * @param OutError Optional, pointer to string where error will be logged if update fails.
+	 * @return true of path could be resolved against the base value, and instance types were updated.
+	 */
+	bool UpdateSegmentsFromValue(const FEzAbilityDataView BaseValueView, FString* OutError = nullptr);
+
+	UE_DEPRECATED(5.3, "Use UpdateSegmentsFromValue instead")
+	bool UpdateInstanceStructsFromValue(const FEzAbilityDataView BaseValueView, FString* OutError = nullptr)
+	{
+		return UpdateSegmentsFromValue(BaseValueView, OutError);
+	}
+	
+	/** @return true if the path is empty. In that case the path points to the struct. */
+	bool IsPathEmpty() const { return Segments.IsEmpty(); }
+
+	/** @return true if any of the path segments is and indirection via instanced struct or object. */
+	bool HasAnyInstancedIndirection() const
+	{
+		return Segments.ContainsByPredicate([](const FEzAbilityPropertyPathSegment& Segment)
+		{
+			return Segment.GetInstanceStruct() != nullptr;
+		});
+	}
+
+	/** Reset the path to empty. */
+	void Reset()
+	{
+#if WITH_EDITORONLY_DATA
+		StructID = FGuid();
+#endif
+		Segments.Reset();
+	}
+
+#if WITH_EDITORONLY_DATA
+	const FGuid& GetStructID() const { return StructID; }
+	void SetStructID(const FGuid NewStructID) { StructID = NewStructID; }
+#endif // WITH_EDITORONLY_DATA
+
+	TConstArrayView<FEzAbilityPropertyPathSegment> GetSegments() const { return Segments; }
+	TArrayView<FEzAbilityPropertyPathSegment> GetMutableSegments() { return Segments; }
+	int32 NumSegments() const { return Segments.Num(); }
+	const FEzAbilityPropertyPathSegment& GetSegment(const int32 Index) const { return Segments[Index]; }
+
+	/** Adds a path segment to the path. */
+	void AddPathSegment(const FName InName, const int32 InArrayIndex = INDEX_NONE, const UStruct* InInstanceType = nullptr)
+	{
+		Segments.Emplace(InName, InArrayIndex, InInstanceType);
+	}
+
+	/** Adds a path segment to the path. */
+	void AddPathSegment(const FEzAbilityPropertyPathSegment& PathSegment)
+	{
+		Segments.Add(PathSegment);
+	}
+
+	/** Test if paths are equal. */
+	bool operator==(const FEzAbilityPropertyPath& RHS) const;
+
+private:
+#if WITH_EDITORONLY_DATA
+	/** ID of the struct this property path is relative to. */
+	UPROPERTY()
+	FGuid StructID;
+#endif // WITH_EDITORONLY_DATA
+
+	/** Path segments pointing to a specific property on the path. */
+	UPROPERTY()
+	TArray<FEzAbilityPropertyPathSegment> Segments;
+};
+
+
+USTRUCT()
+struct UE_DEPRECATED(5.3, "Use FEzAbilityPropertyPath instead.") EZABILITY_API FEzAbilityEditorPropertyPath
+{
+	GENERATED_BODY()
+
+	FEzAbilityEditorPropertyPath() = default;
+	FEzAbilityEditorPropertyPath(const FGuid& InStructID, const TCHAR* PropertyName) : StructID(InStructID) { Path.Add(PropertyName); }
+	
+	/**
+	 * Returns the property path as a one string. Highlight allows to decorate a specific segment.
+	 * @param HighlightedSegment Index of the highlighted path segment
+	 * @param HighlightPrefix String to append before highlighted segment
+	 * @param HighlightPostfix String to append after highlighted segment
+	 */
+	FString ToString(const int32 HighlightedSegment = INDEX_NONE, const TCHAR* HighlightPrefix = nullptr, const TCHAR* HighlightPostfix = nullptr) const;
+
+	/** Handle of the struct this property path is relative to. */
+	UPROPERTY()
+	FGuid StructID;
+
+	/** Property path segments */
+	UPROPERTY()
+	TArray<FString> Path;
+
+	bool IsValid() const { return StructID.IsValid(); }
+
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	bool operator==(const FEzAbilityEditorPropertyPath& RHS) const;
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+};
+
+
+/**
+ * Representation of a property binding in EzAbility
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyPathBinding
+{
+	GENERATED_BODY()
+
+PRAGMA_DISABLE_DEPRECATION_WARNINGS	
+	FEzAbilityPropertyPathBinding() = default;
+	
+	FEzAbilityPropertyPathBinding(const FEzAbilityPropertyPath& InSourcePath, const FEzAbilityPropertyPath& InTargetPath)
+		: SourcePropertyPath(InSourcePath)
+		, TargetPropertyPath(InTargetPath)
+	{
+	}
+
+	FEzAbilityPropertyPathBinding(const FEzAbilityDataHandle InSourceDataHandle, const FEzAbilityPropertyPath& InSourcePath, const FEzAbilityPropertyPath& InTargetPath)
+		: SourcePropertyPath(InSourcePath)
+		, TargetPropertyPath(InTargetPath)
+		, SourceDataHandle(InSourceDataHandle)
+	{
+	}
+
+	UE_DEPRECATED(5.4, "Use constructor with DataHandle instead.")
+	FEzAbilityPropertyPathBinding(const FEzAbilityIndex16 InCompiledSourceStructIndex, const FEzAbilityPropertyPath& InSourcePath, const FEzAbilityPropertyPath& InTargetPath)
+		: SourcePropertyPath(InSourcePath)
+		, TargetPropertyPath(InTargetPath)
+	{
+	}
+
+	UE_DEPRECATED(5.3, "Use constructor with FEzAbilityPropertyPath instead.")
+	FEzAbilityPropertyPathBinding(const FEzAbilityEditorPropertyPath& InSourcePath, const FEzAbilityEditorPropertyPath& InTargetPath)
+	{
+	}
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+
+	void PostSerialize(const FArchive& Ar);
+
+	const FEzAbilityPropertyPath& GetSourcePath() const { return SourcePropertyPath; }
+	const FEzAbilityPropertyPath& GetTargetPath() const { return TargetPropertyPath; }
+
+	FEzAbilityPropertyPath& GetMutableSourcePath() { return SourcePropertyPath; }
+	FEzAbilityPropertyPath& GetMutableTargetPath() { return TargetPropertyPath; }
+
+	UE_DEPRECATED(5.4, "Use GetSourceDataHandle instead.")
+	FEzAbilityIndex16 GetCompiledSourceStructIndex() const { return {}; }
+
+	void SetSourceDataHandle(const FEzAbilityDataHandle NewSourceDataHandle) { SourceDataHandle = NewSourceDataHandle; }
+	FEzAbilityDataHandle GetSourceDataHandle() const { return SourceDataHandle; }
+
+private:
+	/** Source property path of the binding */
+	UPROPERTY()
+	FEzAbilityPropertyPath SourcePropertyPath;
+
+	/** Target property path of the binding */
+	UPROPERTY()
+	FEzAbilityPropertyPath TargetPropertyPath;
+
+	/** Describes how to get the source data pointer for the binding. */
+	UPROPERTY()
+	FEzAbilityDataHandle SourceDataHandle = FEzAbilityDataHandle::Invalid;
+
+public:
+#if WITH_EDITORONLY_DATA
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	UPROPERTY()
+	FEzAbilityEditorPropertyPath SourcePath_DEPRECATED;
+
+	UPROPERTY()
+	FEzAbilityEditorPropertyPath TargetPath_DEPRECATED;
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+#endif // WITH_EDITORONLY_DATA
+};
+
+template<>
+struct TStructOpsTypeTraits<FEzAbilityPropertyPathBinding> : public TStructOpsTypeTraitsBase2<FEzAbilityPropertyPathBinding>
+{
+	enum 
+	{
+		WithPostSerialize = true,
+	};
+};
+
+using FEzAbilityEditorPropertyBinding UE_DEPRECATED(5.3, "Deprecated struct. Please use FEzAbilityPropertyPathBinding instead.") = FEzAbilityPropertyPathBinding;
+
+/**
+ * Representation of a property reference binding in EzAbility.
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyRefPath
+{
+	GENERATED_BODY()
+
+	FEzAbilityPropertyRefPath() = default;
+
+	FEzAbilityPropertyRefPath(FEzAbilityDataHandle InSourceDataHandle, const FEzAbilityPropertyPath& InSourcePath)
+		: SourcePropertyPath(InSourcePath)
+		, SourceDataHandle(InSourceDataHandle)
+	{
+	}
+
+	const FEzAbilityPropertyPath& GetSourcePath() const { return SourcePropertyPath; }
+
+	FEzAbilityPropertyPath& GetMutableSourcePath() { return SourcePropertyPath; }
+
+	void SetSourceDataHandle(const FEzAbilityDataHandle NewSourceDataHandle) { SourceDataHandle = NewSourceDataHandle; }
+	FEzAbilityDataHandle GetSourceDataHandle() const { return SourceDataHandle; }
+
+private:
+	/** Source property path of the reference */
+	UPROPERTY()
+	FEzAbilityPropertyPath SourcePropertyPath;
+
+	/** Describes how to get the source data pointer */
+	UPROPERTY()
+	FEzAbilityDataHandle SourceDataHandle = FEzAbilityDataHandle::Invalid;
+};
+
+/**
+ * Deprecated. Describes a segment of a property path. Used for storage only.
+ */
+USTRUCT()
+struct UE_DEPRECATED(5.3, "Use FEzAbilityPropertyPath instead.") EZABILITY_API FEzAbilityPropertySegment
+{
+	GENERATED_BODY()
+
+	/** @return true if the segment is empty. */
+	bool IsEmpty() const { return Name.IsNone(); }
+	
+	/** Property name. */
+	UPROPERTY()
+	FName Name;
+
+	/** Index in the array the property points at. */
+	UPROPERTY()
+	FEzAbilityIndex16 ArrayIndex = FEzAbilityIndex16::Invalid;
+
+	/** Index to next segment. */
+	UPROPERTY()
+	FEzAbilityIndex16 NextIndex = FEzAbilityIndex16::Invalid;
+
+	/** Type of access/indirection. */
+	UPROPERTY()
+	EEzAbilityPropertyAccessType Type = EEzAbilityPropertyAccessType::Offset;
+};
+
+
 /**
- * 
+ * Deprecated. Describes property binding, the property from source path is copied into the property at the target path.
  */
-UCLASS()
-class EZABILITY_API UEzAbilityPropertyBindings : public UObject
+USTRUCT()
+struct UE_DEPRECATED(5.3, "Use FEzAbilityPropertyPath instead.") EZABILITY_API FEzAbilityPropertyBinding
 {
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+
 	GENERATED_BODY()
+
+	/** Source property path. */
+	UPROPERTY()
+	FEzAbilityPropertySegment SourcePath;
+
+	/** Target property path. */
+	UPROPERTY()
+	FEzAbilityPropertySegment TargetPath;
+	
+	/** Index to the source struct the source path refers to, sources are stored in FEzAbilityPropertyBindings. */
+	UPROPERTY()
+	FEzAbilityIndex16 SourceStructIndex = FEzAbilityIndex16::Invalid;
+	
+	/** The type of copy to use between the properties. */
+	UPROPERTY()
+	EEzAbilityPropertyCopyType CopyType = EEzAbilityPropertyCopyType::None;
+
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
 };
+
+
+/**
+ * Used internally.
+ * Property indirection is a resolved property path segment, used for accessing properties in structs.
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyIndirection
+{
+	GENERATED_BODY()
+
+	/** Index in the array the property points at. */
+	UPROPERTY()
+	FEzAbilityIndex16 ArrayIndex = FEzAbilityIndex16::Invalid;
+
+	/** Cached offset of the property */
+	UPROPERTY()
+	uint16 Offset = 0;
+
+	/** Cached offset of the property */
+	UPROPERTY()
+	FEzAbilityIndex16 NextIndex = FEzAbilityIndex16::Invalid;
+
+	/** Type of access/indirection. */
+	UPROPERTY()
+	EEzAbilityPropertyAccessType Type = EEzAbilityPropertyAccessType::Offset;
+
+	/** Type of the struct or object instance in case the segment is pointing into an instanced data. */
+	UPROPERTY()
+	TObjectPtr<const UStruct> InstanceStruct = nullptr;
+
+	/** Cached array property. */
+	const FArrayProperty* ArrayProperty = nullptr;
+};
+
+
+/**
+ * Used internally.
+ * Describes property copy, the property from source is copied into the property at the target.
+ * Copy target struct is described in the property copy batch.
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyCopy
+{
+	GENERATED_BODY()
+
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	FEzAbilityPropertyCopy() = default;
+	FEzAbilityPropertyCopy(const FEzAbilityPropertyCopy&) = default;
+	FEzAbilityPropertyCopy(FEzAbilityPropertyCopy&&) = default;
+	FEzAbilityPropertyCopy& operator=(const FEzAbilityPropertyCopy&) = default;
+	FEzAbilityPropertyCopy& operator=(FEzAbilityPropertyCopy&&) = default;
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+
+	/** Source property access. */
+	UPROPERTY()
+	FEzAbilityPropertyIndirection SourceIndirection;
+
+	/** Target property access. */
+	UPROPERTY()
+	FEzAbilityPropertyIndirection TargetIndirection;
+
+	/** Cached pointer to the leaf property of the access. */
+	const FProperty* SourceLeafProperty = nullptr;
+
+	/** Cached pointer to the leaf property of the access. */
+	const FProperty* TargetLeafProperty = nullptr;
+
+	/** Type of the source data, used for validation. */
+	UPROPERTY(Transient)
+	TObjectPtr<const UStruct> SourceStructType = nullptr;
+
+	/** Cached property element size * dim. */
+	UPROPERTY()
+	int32 CopySize = 0;
+
+	/** Describes how to get the source data pointer for the copy. */
+	UPROPERTY()
+	FEzAbilityDataHandle SourceDataHandle = FEzAbilityDataHandle::Invalid;
+
+	/** Type of the copy */
+	UPROPERTY()
+	EEzAbilityPropertyCopyType Type = EEzAbilityPropertyCopyType::None;
+
+	UE_DEPRECATED(5.4, "Use SourceDataHandle instead")
+	UPROPERTY()
+	FEzAbilityIndex16 SourceStructIndex_DEPRECATED = FEzAbilityIndex16::Invalid;
+};
+
+using FEzAbilityPropCopy UE_DEPRECATED(5.3, "Deprecated struct. Please use FEzAbilityPropertyCopy instead.") = FEzAbilityPropertyCopy;
+
+
+/**
+ * Describes a batch of property copies from many sources to one target struct.
+ * Note: The batch is used to reference both bindings and copies (a binding turns into copy when resolved).
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyCopyBatch
+{
+	GENERATED_BODY()
+
+	/** Expected target struct */
+	UPROPERTY()
+	FEzAbilityBindableStructDesc TargetStruct;
+
+	/** Index to first binding/copy. */
+	UPROPERTY()
+	uint16 BindingsBegin = 0;
+
+	/** Index to one past the last binding/copy. */
+	UPROPERTY()
+	uint16 BindingsEnd = 0;
+};
+
+using FEzAbilityPropCopyBatch UE_DEPRECATED(5.3, "Deprecated struct. Please use FEzAbilityPropertyCopy instead.") = FEzAbilityPropertyCopyBatch;
+
+/**
+ * Describes access to referenced property.
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyAccess
+{
+	GENERATED_BODY()
+
+	/** Source property access. */
+	UPROPERTY()
+	FEzAbilityPropertyIndirection SourceIndirection;
+
+	/** Cached pointer to the leaf property of the access. */
+	const FProperty* SourceLeafProperty = nullptr;
+
+	/** Type of the source data, used for validation. */
+	UPROPERTY(Transient)
+	TObjectPtr<const UStruct> SourceStructType = nullptr;
+
+	/** Describes how to get the source data pointer. */
+	UPROPERTY()
+	FEzAbilityDataHandle SourceDataHandle = FEzAbilityDataHandle::Invalid;
+};
+
+/**
+ * Runtime storage and execution of property bindings.
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyBindings
+{
+	GENERATED_BODY()
+
+	/**
+	 * Clears all bindings.
+	 */
+	void Reset();
+
+	/**
+	 * Resolves paths to indirections.
+	 * @return True if resolve succeeded.
+	 */
+	[[nodiscard]] bool ResolvePaths();
+
+	/**
+	 * @return True if bindings have been resolved.
+	 */
+	bool IsValid() const { return bBindingsResolved; }
+
+	/**
+	 * @return Number of source structs the copy expects.
+	 */
+	int32 GetSourceStructNum() const { return SourceStructs.Num(); }
+
+	/**
+	 * Copies a property from Source to Target based on the provided Copy.
+	 * @param Copy Describes which parameter and how is copied.
+	 * @param SourceStructView Pointer and type for the source containing the property to be copied.
+	 * @param TargetStructView Pointer and type for the target containing the property to be copied.
+	 * @return true if the property was copied successfully.
+	 */
+	bool CopyProperty(const FEzAbilityPropertyCopy& Copy, FEzAbilityDataView SourceStructView, FEzAbilityDataView TargetStructView) const;
+
+	/** @return copy batch at specified index. */
+	const FEzAbilityPropertyCopyBatch& GetBatch(const FEzAbilityIndex16 TargetBatchIndex) const
+	{
+		check(TargetBatchIndex.IsValid());
+		return CopyBatches[TargetBatchIndex.Get()];
+	}
+
+	/** @return All the property copies for a specific batch. */
+	TConstArrayView<FEzAbilityPropertyCopy> GetBatchCopies(const FEzAbilityIndex16 TargetBatchIndex) const
+	{
+		return GetBatchCopies(GetBatch(TargetBatchIndex));
+	}
+
+	/** @return All the property copies for a specific batch. */
+	TConstArrayView<FEzAbilityPropertyCopy> GetBatchCopies(const FEzAbilityPropertyCopyBatch& Batch) const
+	{
+		const int32 Count = (int32)Batch.BindingsEnd - (int32)Batch.BindingsBegin;
+		if (Count == 0)
+		{
+			return {};
+		}
+		return MakeArrayView(&PropertyCopies[Batch.BindingsBegin], Count);
+	}
+
+	/**
+	 * @return Referenced property access for provided PropertyRef.
+	 */
+	const FEzAbilityPropertyAccess* GetPropertyAccess(const FEzAbilityPropertyRef& Reference) const;
+
+	/**
+	 * Pointer to referenced property 
+	 * @param SourceView Data view to referenced property's owner.
+	 * @param PropertyAccess Access to the property for which we want to obtain a pointer.
+	 * @return Pointer to referenced property if it's type match, nullptr otherwise.
+	 */
+	template< class T >
+	T* GetMutablePropertyPtr(FEzAbilityDataView SourceView, const FEzAbilityPropertyAccess& PropertyAccess) const
+	{
+		check(SourceView.GetStruct() == PropertyAccess.SourceStructType);
+
+		if (!UE::EzAbility::PropertyRefHelpers::Validator<T>::IsValid(*PropertyAccess.SourceLeafProperty))
+		{
+			return nullptr;
+		}
+
+		return reinterpret_cast<T*>(GetAddress(SourceView, PropertyAccess.SourceIndirection, PropertyAccess.SourceLeafProperty));
+	}
+
+	/**
+	 * Resets copied properties in TargetStructView. Can be used e.g. to erase UObject references.
+	 * @param TargetBatchIndex Batch index to copy (see FEzAbilityPropertyBindingCompiler).
+	 * @param TargetStructView View to struct where properties are copied to.
+	 * @return true if all resets succeeded (a reset can fail e.g. if source or destination struct view is invalid).
+	 */
+	bool ResetObjects(const FEzAbilityIndex16 TargetBatchIndex, FEzAbilityDataView TargetStructView) const;
+
+	/**
+	 * @return true if any of the elements in the property bindings contains any of the structs in the set.
+	 */
+	bool ContainsAnyStruct(const TSet<const UStruct*>& Structs);
+	
+	void DebugPrintInternalLayout(FString& OutString) const;
+
+	/** @return how properties are compatible for copying. */
+	static EEzAbilityPropertyAccessCompatibility GetPropertyCompatibility(const FProperty* FromProperty, const FProperty* ToProperty);
+
+	/**
+	 * Resolves what kind of copy type to use between specified property indirections.
+	 * @param SourceIndirection Property path indirections of the copy source,
+	 * @param TargetIndirection Property path indirections of the copy target.
+	 * @param OutCopy Resulting copy type.
+	 * @return true if copy was resolved, or false if no copy could be resolved between paths.
+	 */
+	[[nodiscard]] static bool ResolveCopyType(const FEzAbilityPropertyPathIndirection& SourceIndirection, const FEzAbilityPropertyPathIndirection& TargetIndirection, FEzAbilityPropertyCopy& OutCopy);
+
+	
+	UE_DEPRECATED(5.3, "Should not be used, will be removed in a future version.")
+	TArrayView<FEzAbilityBindableStructDesc> GetSourceStructs() { return  SourceStructs; };
+
+	UE_DEPRECATED(5.3, "Use GetBatch() instead.")
+	TArrayView<FEzAbilityPropertyCopyBatch> GetCopyBatches() { return CopyBatches; };
+
+	UE_DEPRECATED(5.4, "Use GetBatchCopies() and Copy() instead.")
+	bool CopyTo(TConstArrayView<FEzAbilityDataView> SourceStructViews, const FEzAbilityIndex16 TargetBatchIndex, FEzAbilityDataView TargetStructView) const
+	{
+		return false;
+	}
+
+private:
+	[[nodiscard]] static bool ResolvePath(const UStruct* Struct, const FEzAbilityPropertyPath& Path, TArray<FEzAbilityPropertyIndirection>& OutIndirections, FEzAbilityPropertyIndirection& OutFirstIndirection, FEzAbilityPropertyPathIndirection& OutLeafIndirection);
+	static uint8* GetAddress(FEzAbilityDataView InStructView, TConstArrayView<FEzAbilityPropertyIndirection> Indirections, const FEzAbilityPropertyIndirection& FirstIndirection, const FProperty* LeafProperty);
+	
+	uint8* GetAddress(FEzAbilityDataView InStructView, const FEzAbilityPropertyIndirection& FirstIndirection, const FProperty* LeafProperty) const
+	{
+		return GetAddress(InStructView, PropertyIndirections, FirstIndirection, LeafProperty);
+	}
+
+	[[nodiscard]] bool ResolvePath(const UStruct* Struct, const FEzAbilityPropertyPath& Path, FEzAbilityPropertyIndirection& OutFirstIndirection, FEzAbilityPropertyPathIndirection& OutLeafIndirection)
+	{
+		return ResolvePath(Struct, Path, PropertyIndirections, OutFirstIndirection, OutLeafIndirection);
+	}
+
+	const FEzAbilityBindableStructDesc* GetSourceDescByHandle(const FEzAbilityDataHandle SourceDataHandle);
+
+	void PerformCopy(const FEzAbilityPropertyCopy& Copy, uint8* SourceAddress, uint8* TargetAddress) const;
+	void PerformResetObjects(const FEzAbilityPropertyCopy& Copy, uint8* TargetAddress) const;
+
+	/** Array of expected source structs. */
+	UPROPERTY()
+	TArray<FEzAbilityBindableStructDesc> SourceStructs;
+
+	/** Array of copy batches. */
+	UPROPERTY()
+	TArray<FEzAbilityPropertyCopyBatch> CopyBatches;
+
+	/** Array of property bindings, resolved into arrays of copies before use. */
+	UPROPERTY()
+	TArray<FEzAbilityPropertyPathBinding> PropertyPathBindings;
+
+	/** Array of property copies */
+	UPROPERTY(Transient)
+	TArray<FEzAbilityPropertyCopy> PropertyCopies;
+
+	/** Array of referenced property paths */
+	UPROPERTY()
+	TArray<FEzAbilityPropertyRefPath> PropertyReferencePaths;
+
+	/** Array of individually accessed properties */
+	UPROPERTY()
+	TArray<FEzAbilityPropertyAccess> PropertyAccesses;
+
+	/** Array of property indirections, indexed by accesses*/
+	UPROPERTY(Transient)
+	TArray<FEzAbilityPropertyIndirection> PropertyIndirections;
+
+	/** Flag indicating if the properties has been resolved successfully . */
+	bool bBindingsResolved = false;
+
+#if WITH_EDITORONLY_DATA
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	
+	UE_DEPRECATED_FORGAME(5.3, "Use PropertyPathBindings instead.")
+	UPROPERTY()
+	TArray<FEzAbilityPropertyBinding> PropertyBindings_DEPRECATED;
+
+	UE_DEPRECATED_FORGAME(5.3, "Use PropertyPathBindings instead.")
+	UPROPERTY()
+	TArray<FEzAbilityPropertySegment> PropertySegments_DEPRECATED;
+	
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+#endif // WITH_EDITORONLY_DATA
+
+	friend FEzAbilityPropertyBindingCompiler;
+	friend UEzAbility;
+};
+
+/**
+ * Helper interface to reason about bound properties. The implementation is in the editor plugin.
+ */
+struct EZABILITY_API IEzAbilityBindingLookup
+{
+	/** @return Source path for given target path, or null if binding does not exists. */
+	virtual const FEzAbilityPropertyPath* GetPropertyBindingSource(const FEzAbilityPropertyPath& InTargetPath) const PURE_VIRTUAL(IEzAbilityBindingLookup::GetPropertyBindingSource, return nullptr; );
+
+	/** @return Display name given property path. */
+	virtual FText GetPropertyPathDisplayName(const FEzAbilityPropertyPath& InPath) const PURE_VIRTUAL(IEzAbilityBindingLookup::GetPropertyPathDisplayName, return FText::GetEmpty(); );
+
+	/** @return Leaf property based on property path. */
+	virtual const FProperty* GetPropertyPathLeafProperty(const FEzAbilityPropertyPath& InPath) const PURE_VIRTUAL(IEzAbilityBindingLookup::GetPropertyPathLeafProperty, return nullptr; );
+
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	UE_DEPRECATED(5.3, "Use version with FEzAbilityPropertyPath instead.")
+	virtual const FEzAbilityEditorPropertyPath* GetPropertyBindingSource(const FEzAbilityEditorPropertyPath& InTargetPath) const final { return nullptr; }
+	
+	UE_DEPRECATED(5.3, "Use version with FEzAbilityPropertyPath instead.")
+	virtual FText GetPropertyPathDisplayName(const FEzAbilityEditorPropertyPath& InPath) const final { return FText::GetEmpty(); }
+	
+	UE_DEPRECATED(5.3, "Use version with FEzAbilityPropertyPath instead.")
+	virtual const FProperty* GetPropertyPathLeafProperty(const FEzAbilityEditorPropertyPath& InPath) const final { return nullptr; }
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+};
+
+
+namespace UE::EzAbility
+{
+	/** @return desc and path as a display string. */
+	extern EZABILITY_API FString GetDescAndPathAsString(const FEzAbilityBindableStructDesc& Desc, const FEzAbilityPropertyPath& Path);
+
+#if WITH_EDITOR
+	/**
+	 * Returns property usage based on the Category metadata of given property.
+	 * @param Property Handle to property where value is got from.
+	 * @return found usage type, or EEzAbilityPropertyUsage::Invalid if not found.
+	 */
+	EZABILITY_API EEzAbilityPropertyUsage GetUsageFromMetaData(const FProperty* Property);
+#endif
+} // UE::EzAbility
+
+namespace UE::EzAbility::Private
+{
+#if WITH_EDITORONLY_DATA
+// Helper functions to convert between property path types.
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	UE_DEPRECATED(5.3, "Will be removed when FEzAbilityEditorPropertyPath is removed.")
+	extern EZABILITY_API FEzAbilityPropertyPath ConvertEditorPath(const FEzAbilityEditorPropertyPath& InEditorPath);
+	UE_DEPRECATED(5.3, "Will be removed when FEzAbilityEditorPropertyPath is removed.")
+	extern EZABILITY_API FEzAbilityEditorPropertyPath ConvertEditorPath(const FEzAbilityPropertyPath& InPath);
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+#endif // WITH_EDITORONLY_DATA
+} // UE::EzAbility::Private 
+
+#if UE_ENABLE_INCLUDE_ORDER_DEPRECATED_IN_5_2
+#include "CoreMinimal.h"
+#endif

+ 186 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityPropertyRef.h

@@ -0,0 +1,186 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "EzAbility.h"
+#include "EzAbilityIndexTypes.h"
+#include "EzAbilityContext.h"
+#include "EzAbilityPropertyBindings.h"
+#include "EzAbilityPropertyRef.generated.h"
+
+struct FEzAbilityPropertyRef;
+
+namespace UE::EzAbility::PropertyRefHelpers
+{
+    /**
+	 * @param PropertyRef Property's reference to get pointer to.
+	 * @param InstanceDataStorage Instance Data Storage
+	 * @param ExecutionFrame Execution frame owning referenced property
+	 * @param ParentExecutionFrame Parent of execution frame owning referenced property
+	 * @return Pointer to referenced property if succeeded.
+	 */
+	template<class T>
+	static T* GetMutablePtrToProperty(const FEzAbilityPropertyRef& PropertyRef, FEzAbilityInstanceStorage& InstanceDataStorage, const FEzAbilityExecutionFrame& ExecutionFrame, const FEzAbilityExecutionFrame* ParentExecutionFrame)
+	{
+		const FEzAbilityPropertyBindings& PropertyBindings = ExecutionFrame.Ability->GetPropertyBindings();
+		if (const FEzAbilityPropertyAccess* PropertyAccess = PropertyBindings.GetPropertyAccess(PropertyRef))
+		{
+			// Passing empty ContextAndExternalDataViews, as PropertyRef is not allowed to point to context or external data.
+			FEzAbilityDataView SourceView = FEzAbilityContext::GetDataView(InstanceDataStorage, nullptr, ParentExecutionFrame, ExecutionFrame, {}, PropertyAccess->SourceDataHandle);
+			return PropertyBindings.GetMutablePropertyPtr<T>(SourceView, *PropertyAccess);
+		}
+
+		return nullptr;
+	}
+}
+
+/**
+ * Property ref allows to get a pointer to selected property in EzAbility.
+ * The expected type of the reference should be set in "RefType" meta specifier.
+ *
+ * Meta specifiers for the type:
+ *  - RefType = "<type>"
+ *		- Specifies the type of property to reference.
+ *		- Supported types are: bool, byte, int32, int64, float, double, Name, String, Text, UObject pointers, and structs.
+ *  - IsRefToArray
+ *		- If specified, the reference is to an TArray<RefType>
+ *  - Optional
+ *		- If specified, the reference can be left unbound, otherwise the compiler report error if the reference is not bound.
+ *
+ * Example:
+ *
+ *  // Reference to float
+ *	UPROPERTY(EditAnywhere, meta = (RefType = "float"))
+ *	FEzAbilityPropertyRef RefToFloat;
+ * 
+ *  // Reference to FTestStructBase
+ *	UPROPERTY(EditAnywhere, meta = (RefType = "/Script/ModuleName.TestStructBase"))
+ *	FEzAbilityPropertyRef RefToTest;
+ *
+ *  // Reference to TArray<FTestStructBase>
+ *	UPROPERTY(EditAnywhere, meta = (RefType = "/Script/ModuleName.TestStructBase", IsRefToArray))
+ *	FEzAbilityPropertyRef RefToArrayOfTests;
+ */
+USTRUCT()
+struct EZABILITY_API FEzAbilityPropertyRef
+{
+	GENERATED_BODY()
+
+	FEzAbilityPropertyRef() = default;
+
+	/** @return pointer to the property if possible, nullptr otherwise. */
+	template<class T>
+	T* GetMutablePtr(FEzAbilityContext& Context) const
+	{
+		const FEzAbilityExecutionFrame* CurrentlyProcessedFrame = Context.GetCurrentlyProcessedFrame();
+		check(CurrentlyProcessedFrame);
+
+		return UE::EzAbility::PropertyRefHelpers::GetMutablePtrToProperty<T>(*this, Context.GetMutableInstanceData()->GetMutableStorage(), *CurrentlyProcessedFrame, Context.GetCurrentlyProcessedParentFrame());
+	}
+
+	/**
+	 * Used internally.
+	 * @return index to referenced property access
+	 */
+	FEzAbilityIndex16 GetRefAccessIndex() const
+	{
+		return RefAccessIndex;
+	}
+
+private:
+	UPROPERTY()
+	FEzAbilityIndex16 RefAccessIndex;
+
+	friend FEzAbilityPropertyBindingCompiler;
+};
+
+/**
+ * TEzAbilityPropertyRef is a type-safe FEzAbilityPropertyRef wrapper against the given type.
+ * @note When used as a property, this automatically defines PropertyRef property meta-data.
+ * 
+ * Example:
+ *
+ *  // Reference to float
+ *	UPROPERTY(EditAnywhere)
+ *	TEzAbilityPropertyRef<float> RefToFloat;
+ * 
+ *  // Reference to FTestStructBase
+ *	UPROPERTY(EditAnywhere)
+ *	TEzAbilityPropertyRef<FTestStructBase> RefToTest;
+ *
+ *  // Reference to TArray<FTestStructBase>
+ *	UPROPERTY(EditAnywhere)
+ *	TEzAbilityPropertyRef<TArray<FTestStructBase>> RefToArrayOfTests;
+ */
+template<class TRef>
+struct TEzAbilityPropertyRef
+{
+	/** @return pointer to the property if possible, nullptr otherwise. */
+	TRef* GetMutablePtr(FEzAbilityContext& Context) const
+	{
+		return PropertyRef.GetMutablePtr<TRef>(Context);
+	}
+
+	/**
+	 * Used internally.
+	 * @return internal property ref
+	 */
+	FEzAbilityPropertyRef GetInternalPropertyRef() const
+	{
+		return PropertyRef;
+	}
+
+private:
+	FEzAbilityPropertyRef PropertyRef;
+};
+
+/**
+ * External Handle allows to wrap-up property reference to make it accessible without having an access to EzAbilityExecutionContext. Useful for capturing property reference in callbacks.
+ */
+template<class TRef>
+struct TEzAbilityPropertyRefExternalHandle
+{
+	TEzAbilityPropertyRefExternalHandle(FEzAbilityPropertyRef InPropertyRef, FEzAbilityContext& InContext)
+		: WeakInstanceStorage(InContext.GetMutableInstanceData()->GetWeakMutableStorage())
+		, WeakEzAbility(InContext.GetCurrentlyProcessedFrame()->Ability)
+		, RootState(InContext.GetCurrentlyProcessedFrame()->RootState)
+		, PropertyRef(InPropertyRef)
+	{}
+
+	TEzAbilityPropertyRefExternalHandle(TEzAbilityPropertyRef<TRef> InPropertyRef, FEzAbilityContext& InContext)
+		: TEzAbilityPropertyRefExternalHandle(InPropertyRef.GetInternalPropertyRef(), InContext)
+	{}
+
+	/** @return pointer to the property if possible, nullptr otherwise. */
+	TRef* GetMutablePtr() const
+	{
+		if (!WeakInstanceStorage.IsValid())
+		{
+			return nullptr;
+		}
+
+		FEzAbilityInstanceStorage& InstanceStorage = *WeakInstanceStorage.Pin();
+
+		const TArray<FEzAbilityExecutionFrame>& ActiveFrames = InstanceStorage.GetExecutionState().ActiveFrames;
+		const int32 FrameIndex = ActiveFrames.IndexOfByPredicate([this](const FEzAbilityExecutionFrame& Frame)
+		{
+			return Frame.RootState == RootState && Frame.Ability == WeakEzAbility;
+		});
+
+		if (FrameIndex == INDEX_NONE)
+		{
+			return nullptr;
+		}
+
+		const FEzAbilityExecutionFrame& Frame = ActiveFrames[FrameIndex];
+		const FEzAbilityExecutionFrame* ParentFrame = FrameIndex > 0 ? &ActiveFrames[FrameIndex - 1] : nullptr;
+
+		return UE::EzAbility::PropertyRefHelpers::GetMutablePtrToProperty<TRef>(PropertyRef, InstanceStorage, Frame, ParentFrame);
+	}
+
+private:
+	TWeakPtr<FEzAbilityInstanceStorage> WeakInstanceStorage;
+	TWeakObjectPtr<const UEzAbility> WeakEzAbility = nullptr;
+	FEzAbilityStateHandle RootState = FEzAbilityStateHandle::Invalid;
+	FEzAbilityPropertyRef PropertyRef;
+};

+ 171 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityPropertyRefHelpers.h

@@ -0,0 +1,171 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "UObject/Field.h"
+#include "UObject/TextProperty.h"
+#include "UObject/EnumProperty.h"
+
+#if WITH_EDITOR
+#include "Containers/ContainersFwd.h"
+#include "Templates/SharedPointerFwd.h"
+#endif
+
+class FProperty;
+struct FBindingChainElement;
+struct FEzAbilityBindableStructDesc;
+struct FEzAbilityPropertyPathIndirection;
+struct FEdGraphPinType;
+
+namespace UE::EzAbility::PropertyRefHelpers
+{
+#if WITH_EDITOR
+	/**
+	 * @param RefProperty Property of PropertyRef type
+	 * @param SourceProperty Property to check it's type compatibility
+	 * @return true if SourceProperty type is compatible with PropertyRef
+	 */
+	bool EZABILITY_API IsPropertyRefCompatibleWithProperty(const FProperty& RefProperty, const FProperty& SourceProperty);
+
+	/**
+	 * @param SourcePropertyPathIndirections Path indirections of the referenced property
+	 * @param SourceStruct Bindable owner of referenced property
+	 * @return true if property can be referenced by PropertyRef
+	 */
+	bool EZABILITY_API IsPropertyAccessibleForPropertyRef(TConstArrayView<FEzAbilityPropertyPathIndirection> SourcePropertyPathIndirections, FEzAbilityBindableStructDesc SourceStruct);
+
+	/**
+	 * @param SourceProperty Referenced property
+	 * @param BindingChain Binding chain to referenced property
+	 * @param SourceStruct Bindable owner of referenced property
+	 * @return true if property can be referenced by PropertyRef
+	 */
+	bool EZABILITY_API IsPropertyAccessibleForPropertyRef(const FProperty& SourceProperty, TConstArrayView<FBindingChainElement> BindingChain, FEzAbilityBindableStructDesc SourceStruct);
+
+	/**
+	 * @param Property Property to check
+	 * @return true if Property is a PropertyRef
+	 */
+	bool EZABILITY_API IsPropertyRef(const FProperty& Property);
+
+	/**
+	 * @param RefProperty Property of PropertyRef type
+	 * @return PinType for PropertyRef's internal type
+	 */
+	FEdGraphPinType EZABILITY_API GetPropertyRefInternalTypeAsPin(const FProperty& RefProperty);
+#endif
+
+	template<class T, class = void>
+	struct Validator
+	{};
+
+	template<>
+	struct Validator<FBoolProperty::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FBoolProperty>(); };
+	};
+
+	template<>
+	struct Validator<FByteProperty::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FByteProperty>(); };
+	};
+
+	template<>
+	struct Validator<FIntProperty::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FIntProperty>(); };
+	};
+
+	template<>
+	struct Validator<FInt64Property::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FInt64Property>(); };
+	};
+
+	template<>
+	struct Validator<FFloatProperty::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FFloatProperty>(); };
+	};
+
+	template<>
+	struct Validator<FDoubleProperty::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FDoubleProperty>(); };
+	};
+
+	template<>
+	struct Validator<FNameProperty::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FNameProperty>(); };
+	};
+
+	template<>
+	struct Validator<FStrProperty::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FStrProperty>(); };
+	};
+
+	template<>
+	struct Validator<FTextProperty::TCppType>
+	{
+		static bool IsValid(const FProperty& Property){ return Property.IsA<FTextProperty>(); };
+	};
+
+	template<class T>
+	struct Validator<T, typename TEnableIf<TIsTArray_V<T>, void>::Type>
+	{
+		static bool IsValid(const FProperty& Property)
+		{
+			if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(&Property))
+			{
+				return Validator<typename T::ElementType>::IsValid(*ArrayProperty->Inner);
+			}
+
+			return false;
+		}
+	};
+
+	template<class T>
+	struct Validator<T, decltype(TBaseStructure<T>::Get, void())>
+	{
+		static bool IsValid(const FProperty& Property)
+		{		
+			if (const FStructProperty* StructProperty = CastField<FStructProperty>(&Property))
+			{
+				return StructProperty->Struct->IsChildOf(TBaseStructure<T>::Get());
+			}
+
+			return false;
+		}
+	};
+
+	template<class T>
+	struct Validator<T, typename TEnableIf<TIsDerivedFrom<typename TRemovePointer<T>::Type, UObject>::IsDerived>::Type>
+	{
+		static bool IsValid(const FProperty& Property)
+		{		
+			if (const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(&Property))
+			{
+				return ObjectProperty->PropertyClass == TRemovePointer<T>::Type::StaticClass();
+			}
+
+			return false;
+		}
+	};
+
+	template<class T>
+	struct Validator<T, typename TEnableIf<TIsEnum<T>::Value>::Type>
+	{
+		static bool IsValid(const FProperty& Property)
+		{		
+			if (const FEnumProperty* EnumProperty = CastField<FEnumProperty>(&Property))
+			{
+				return EnumProperty->GetEnum() == StaticEnum<T>();
+			}
+
+			return false;
+		}
+	};
+}

+ 52 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilitySchema.h

@@ -0,0 +1,52 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "UObject/Object.h"
+#include "EzAbilitySchema.generated.h"
+
+struct FEzAbilityExternalDataDesc;
+
+/**
+ * 
+ */
+UCLASS(Abstract)
+class EZABILITY_API UEzAbilitySchema : public UObject
+{
+	GENERATED_BODY()
+
+public:
+
+	/** @return True if specified struct is supported */
+	virtual bool IsStructAllowed(const UScriptStruct* InScriptStruct) const { return false; }
+
+	/** @return True if specified class is supported */
+	virtual bool IsClassAllowed(const UClass* InScriptStruct) const { return false; }
+
+	/** @return True if specified struct/class is supported as external data */
+	virtual bool IsExternalItemAllowed(const UStruct& InStruct) const { return false; }
+
+	/**
+	 * Helper function to check if a class is any of the Blueprint extendable item classes (Eval, Task, Condition).
+	 * Can be used to quickly accept all of those classes in IsClassAllowed().
+	 * @return True if the class is a EzAbility item Blueprint base class.
+	 */
+	bool IsChildOfBlueprintBase(const UClass* InClass) const;
+
+	/** @return List of context objects (UObjects or UScriptStructs) enforced by the schema. They must be provided at runtime through the execution context. */
+	virtual TConstArrayView<FEzAbilityExternalDataDesc> GetContextDataDescs() const { return {}; }
+
+#if WITH_EDITOR
+	
+	/** @return True if enter conditions are allowed. */
+	virtual bool AllowEnterConditions() const { return true; }
+
+	/** @return True if evaluators are allowed. */
+	virtual bool AllowEvaluators() const { return true; }
+
+	/** @return True if multiple tasks are allowed. */
+	virtual bool AllowMultipleTasks() const { return true; }
+	
+#endif // WITH_EDITOR
+};

+ 367 - 23
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityTypes.h

@@ -4,36 +4,82 @@
 
 #include "CoreMinimal.h"
 #include "EzAbilityIndexTypes.h"
+#include "GameplayTagContainer.h"
 #include "PropertyBag.h"
 #include "UObject/Object.h"
 #include "EzAbilityTypes.generated.h"
 
+class UEzAbility;
+
 UENUM()
-enum class EEzAbilityTraceEventType : uint8
+enum class EEzAbilityExternalDataRequirement : uint8
 {
-	Unset,
-	OnEntering				UMETA(DisplayName = "Entering"),
-	OnEntered				UMETA(DisplayName = "Entered"),
-	OnExiting				UMETA(DisplayName = "Exiting"),
-	OnExited				UMETA(DisplayName = "Exited"),
-	Push					UMETA(DisplayName = "Push"),
-	Pop						UMETA(DisplayName = "Pop"),
-	OnStateSelected			UMETA(DisplayName = "Selected"),
-	OnStateCompleted		UMETA(DisplayName = "Completed"),
-	OnTicking				UMETA(DisplayName = "Tick"),
-	OnTaskCompleted			UMETA(DisplayName = "Completed"),
-	OnTicked				UMETA(DisplayName = "Ticked"),
-	Passed					UMETA(DisplayName = "Passed"),
-	Failed					UMETA(DisplayName = "Failed"),
-	ForcedSuccess			UMETA(DisplayName = "Forced Success"),
-	ForcedFailure			UMETA(DisplayName = "Forced Failure"),
-	InternalForcedFailure	UMETA(DisplayName = "Internal Forced Failure"),
-	OnEvaluating			UMETA(DisplayName = "Evaluating"),
-	OnTransition			UMETA(DisplayName = "Transition"),
-	OnAbilityStarted		UMETA(DisplayName = "Ability Started"),
-	OnAbilityStopped		UMETA(DisplayName = "Ability Stopped")
+	Required,	// EzAbility cannot be executed if the data is not present.
+	Optional,	// Data is optional for EzAbility execution.
+};
+
+/** Transitions behavior. */
+UENUM()
+enum class EEzAbilityTransitionType : uint8
+{
+	/** No transition will take place. */
+	None,
+
+	/** Stop State Tree or sub-tree and mark execution succeeded. */
+	Succeeded,
+	
+	/** Stop State Tree or sub-tree and mark execution failed. */
+	Failed,
+	
+	/** Transition to the specified state. */
+	GotoState,
+	
+	/** Transition to the next sibling state. */
+	NextState,
+
+	/** Transition to the next selectable sibling state */
+	NextSelectableState,
+
+	NotSet UE_DEPRECATED(5.0, "Use None instead."),
+};
+
+/** Operand between conditions */
+UENUM()
+enum class EEzAbilityConditionOperand : uint8
+{
+	/** Copy result */
+	Copy UMETA(Hidden),
+	
+	/** Combine results with AND. */
+	And,
+	
+	/** Combine results with OR. */
+	Or,
+};
+
+UENUM()
+enum class EEzAbilityTransitionTrigger : uint8
+{
+	None = 0 UMETA(Hidden),
+
+	/** Try trigger transition when a state succeeded or failed. */
+	OnStateCompleted = 0x1 | 0x2,
 
+	/** Try trigger transition when a state succeeded. */
+	OnStateSucceeded = 0x1,
+
+	/** Try trigger transition when a state failed. */
+	OnStateFailed = 0x2,
+
+	/** Try trigger transition each State Tree tick. */
+	OnTick = 0x4,
+	
+	/** Try trigger transition on specific State Tree event. */
+	OnEvent = 0x8,
+
+	MAX
 };
+ENUM_CLASS_FLAGS(EEzAbilityTransitionTrigger)
 
 UENUM(BlueprintType)
 enum class EEzAbilityTransitionPriority : uint8
@@ -53,6 +99,13 @@ enum class EEzAbilityTransitionPriority : uint8
 	Critical,
 };
 
+inline bool operator<(const EEzAbilityTransitionPriority Lhs, const EEzAbilityTransitionPriority Rhs) { return static_cast<uint8>(Lhs) < static_cast<uint8>(Rhs); }
+inline bool operator>(const EEzAbilityTransitionPriority Lhs, const EEzAbilityTransitionPriority Rhs) { return static_cast<uint8>(Lhs) > static_cast<uint8>(Rhs); }
+inline bool operator<=(const EEzAbilityTransitionPriority Lhs, const EEzAbilityTransitionPriority Rhs) { return static_cast<uint8>(Lhs) <= static_cast<uint8>(Rhs); }
+inline bool operator>=(const EEzAbilityTransitionPriority Lhs, const EEzAbilityTransitionPriority Rhs) { return static_cast<uint8>(Lhs) >= static_cast<uint8>(Rhs); }
+inline bool operator==(const EEzAbilityTransitionPriority Lhs, const EEzAbilityTransitionPriority Rhs) { return static_cast<uint8>(Lhs) == static_cast<uint8>(Rhs); }
+inline bool operator!=(const EEzAbilityTransitionPriority Lhs, const EEzAbilityTransitionPriority Rhs) { return static_cast<uint8>(Lhs) != static_cast<uint8>(Rhs); }
+
 UENUM(BlueprintType)
 enum class EAbilityRunStatus : uint8
 {
@@ -63,6 +116,42 @@ enum class EAbilityRunStatus : uint8
 	Unset,
 };
 
+UENUM()
+enum class EEzAbilityPropertyUsage : uint8
+{
+	Invalid,
+	Context,
+	Input,
+	Parameter,
+	Output,
+};
+
+UENUM()
+enum class EEzAbilityStateSelectionBehavior : uint8
+{
+	/** The State cannot be directly selected. */
+	None,
+
+	/** When state is considered for selection, it is selected even if it has child states. */
+	TryEnterState UMETA(DisplayName = "Try Enter"),
+
+	/** When state is considered for selection, try to selects the first child state (in order they appear in the child list). If no child states are present, behaves like SelectState. */
+	TrySelectChildrenInOrder UMETA(DisplayName = "Try Select Children In Order"),
+	
+	/** When state is considered for selection, try to trigger the transitions instead. */
+	TryFollowTransitions UMETA(DisplayName = "Try Follow Transitions"),
+};
+
+UENUM()
+enum class EEzAbilitySelectionFallback : uint8
+{
+	/** No fallback */
+	None,
+
+	/** Find next selectable sibling, if any, and select it */
+	NextSelectableSibling,
+};
+
 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 ///State
 UENUM()
@@ -310,7 +399,13 @@ struct EZABILITY_API FCompactEzAbilityState
 	
 	UPROPERTY()
 	FName Name;
+	
+	UPROPERTY()
+	FEzAbilityStateHandle LinkedState = FEzAbilityStateHandle::Invalid; 
 
+	UPROPERTY()
+	TObjectPtr<UEzAbility> LinkedAsset = nullptr;
+	
 	UPROPERTY()
 	FEzAbilityStateHandle Parent = FEzAbilityStateHandle::Invalid;
 	
@@ -346,6 +441,9 @@ struct EZABILITY_API FCompactEzAbilityState
 
 	UPROPERTY()
 	FEzAbilityIndex16 ParameterTemplateIndex = FEzAbilityIndex16::Invalid;
+
+	UPROPERTY()
+	EEzAbilityStateSelectionBehavior SelectionBehavior = EEzAbilityStateSelectionBehavior::TrySelectChildrenInOrder;
 };
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -366,6 +464,66 @@ struct EZABILITY_API FCompactEzAbilityParameters
 	FInstancedPropertyBag Parameters;
 };
 
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+USTRUCT()
+struct EZABILITY_API FEzAbilityRandomTimeDuration
+{
+	GENERATED_BODY()
+
+	/** Reset duration to empty. */
+	void Reset()
+	{
+		Duration = 0;
+		RandomVariance = 0;
+	}
+
+	/** Sets the time duration with random variance. */
+	void Set(const float InDuration, const float InRandomVariance)
+	{
+		Duration = Quantize(InDuration);
+		RandomVariance = Quantize(InRandomVariance);
+	}
+
+	/** @return the fixed duration. */
+	float GetDuration() const
+	{
+		return Duration / Scale;
+	}
+
+	/** @return the maximum random variance. */
+	float GetRandomVariance() const
+	{
+		return Duration / Scale;
+	}
+
+	/** @return True of the duration is empty (always returns 0). */
+	bool IsEmpty() const { return Duration == 0 && RandomVariance == 0; }
+	
+	/** @return Returns random duration around Duration, varied by +-RandomVariation. */
+	float GetRandomDuration() const
+	{
+		const int32 MinVal = FMath::Max(0, static_cast<int32>(Duration) - static_cast<int32>(RandomVariance));
+		const int32 MaxVal = static_cast<int32>(Duration) + static_cast<int32>(RandomVariance);
+		return static_cast<decltype(Scale)>(FMath::RandRange(MinVal, MaxVal)) / Scale;
+	}
+	
+protected:
+
+	static constexpr float Scale = 100.0f;
+
+	uint16 Quantize(const float Value) const
+	{
+		return (uint16)FMath::Clamp(FMath::RoundToInt32(Value * Scale), 0, (int32)MAX_uint16);
+	}
+	
+	UPROPERTY(EditDefaultsOnly, Category = Default)
+	uint16 Duration = 0;
+
+	UPROPERTY(EditDefaultsOnly, Category = Default)
+	uint16 RandomVariance = 0;
+};
+
 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 ///Transition
 USTRUCT()
@@ -373,10 +531,111 @@ struct EZABILITY_API FCompactEzAbilityTransition
 {
 	GENERATED_BODY()
 
-	FCompactEzAbilityTransition();
+	explicit FCompactEzAbilityTransition()
+		: bTransitionEnabled(true)
+	{
+	}
+
+	/** @return True if the transition has delay. */
+	bool HasDelay() const
+	{
+		return !Delay.IsEmpty();
+	}
 	
+	/** Transition event tag, used when trigger type is event. */
+	UPROPERTY()
+	FGameplayTag EventTag;
+
+	/** Index to first condition to test */
+	UPROPERTY()
+	uint16 ConditionsBegin = 0;
+
+	/** Target state of the transition */
 	UPROPERTY()
 	FEzAbilityStateHandle State = FEzAbilityStateHandle::Invalid;
+
+	/** Transition delay. */
+	UPROPERTY()
+	FEzAbilityRandomTimeDuration Delay;
+	
+	/* Type of the transition trigger. */
+	UPROPERTY()
+	EEzAbilityTransitionTrigger Trigger = EEzAbilityTransitionTrigger::None;
+
+	/* Priority of the transition. */
+	UPROPERTY()
+	EEzAbilityTransitionPriority Priority = EEzAbilityTransitionPriority::Normal;
+
+	/** Fallback of the transition if it fails to select the target state */
+	UPROPERTY()
+	EEzAbilitySelectionFallback Fallback = EEzAbilitySelectionFallback::None;
+
+	/** Number of conditions to test. */
+	UPROPERTY()
+	uint8 ConditionsNum = 0;
+
+	/** Indicates if the transition is enabled and should be considered. */
+	UPROPERTY()
+	uint8 bTransitionEnabled : 1;
+};
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+///StructRef
+USTRUCT(BlueprintType)
+struct EZABILITY_API FEzAbilityStructRef
+{
+	GENERATED_BODY()
+
+	FEzAbilityStructRef() = default;
+
+	/** @return true if the reference is valid (safe to use the reference getters). */
+	bool IsValid() const
+	{
+		return Data.IsValid();
+	}
+
+	/** Sets the struct ref (used by property copy) */
+	void Set(FStructView NewData)
+	{
+		Data = NewData;
+	}
+
+	/** Returns const reference to the struct, this getter assumes that all data is valid. */
+	template <typename T>
+	const T& Get() const
+	{
+		return Data.template Get<T>();
+	}
+
+	/** Returns const pointer to the struct, or nullptr if cast is not valid. */
+	template <typename T>
+	const T* GetPtr() const
+	{
+		return Data.template GetPtr<T>();
+	}
+
+	/** Returns mutable reference to the struct, this getter assumes that all data is valid. */
+	template <typename T>
+	T& GetMutable()
+	{
+		return Data.template Get<T>();
+	}
+
+	/** Returns mutable pointer to the struct, or nullptr if cast is not valid. */
+	template <typename T>
+	T* GetMutablePtr()
+	{
+		return Data.template GetPtr<T>();
+	}
+
+	/** @return Struct describing the data type. */
+	const UScriptStruct* GetScriptStruct() const
+	{
+		return Data.GetScriptStruct();
+	}
+
+protected:
+	FStructView Data;
 };
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -543,4 +802,89 @@ protected:
 
 	/** Memory pointing at the class or struct */
 	uint8* Memory = nullptr;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////
+USTRUCT()
+struct EZABILITY_API FEzAbilityNodeIdToIndex
+{
+	GENERATED_BODY()
+
+	FEzAbilityNodeIdToIndex() = default;
+	explicit FEzAbilityNodeIdToIndex(const FGuid& Id, const FEzAbilityIndex16 Index)
+		: Id(Id)
+		, Index(Index)
+	{
+	}
+
+	UPROPERTY();
+	FGuid Id;
+
+	UPROPERTY();
+	FEzAbilityIndex16 Index;
+};
+
+/**
+ * Link to another state in EzAbility
+ */
+USTRUCT(BlueprintType)
+struct EZABILITY_API FEzAbilityStateLink
+{
+	GENERATED_BODY()
+
+	FEzAbilityStateLink() = default;
+
+#if WITH_EDITORONLY_DATA
+	FEzAbilityStateLink(const EEzAbilityTransitionType InType) : LinkType(InType) {}
+
+	UE_DEPRECATED(5.2, "Use UEzAbilityState::GetLinkToState() instead.")
+	void Set(const EEzAbilityTransitionType InType, const class UEzAbilityState* InState = nullptr) {}
+	UE_DEPRECATED(5.2, "Use UEzAbilityState::GetLinkToState() instead.")
+	void Set(const class UStateTreeState* InState) {}
+#endif // WITH_EDITORONLY_DATA
+	
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	FEzAbilityStateLink(const FEzAbilityStateLink& Other) = default;
+	FEzAbilityStateLink(FEzAbilityStateLink&& Other) = default;
+	FEzAbilityStateLink& operator=(FEzAbilityStateLink const& Other) = default;
+	FEzAbilityStateLink& operator=(FEzAbilityStateLink&& Other) = default;
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+	
+	bool Serialize(FStructuredArchive::FSlot Slot);
+	void PostSerialize(const FArchive& Ar);
+
+#if WITH_EDITORONLY_DATA
+	UE_DEPRECATED(5.2, "Will be removed in later releases.")
+	bool IsValid() const { return ID.IsValid(); }
+	
+	/** Name of the state at the time of linking, used for error reporting. */
+	UPROPERTY(EditDefaultsOnly, Category = "Link")
+	FName Name;
+
+	/** ID of the state linked to. */
+	UPROPERTY(EditDefaultsOnly, Category = "Link")
+	FGuid ID;
+
+	/** Type of the transition, used at edit time to describe e.g. next state (which is calculated at compile time). */
+	UPROPERTY(EditDefaultsOnly, Category = "Link")
+	EEzAbilityTransitionType LinkType = EEzAbilityTransitionType::None;
+
+	UE_DEPRECATED(5.2, "Use LinkType instead.")
+	UPROPERTY()
+	EEzAbilityTransitionType Type_DEPRECATED = EEzAbilityTransitionType::GotoState;
+#endif // WITH_EDITORONLY_DATA
+
+	/** Handle of the linked state. */
+	UPROPERTY()
+	FEzAbilityStateHandle StateHandle; 
+};
+
+template<>
+struct TStructOpsTypeTraits<FEzAbilityStateLink> : public TStructOpsTypeTraitsBase2<FEzAbilityStateLink>
+{
+	enum
+	{
+		WithStructuredSerializer = true,
+		WithPostSerialize = true,
+	};
 };

+ 0 - 17
Ability/Plugins/EzAbility/Source/EzAbility/Public/Transition/EzAbilityTransition.h

@@ -1,17 +0,0 @@
-// Fill out your copyright notice in the Description page of Project Settings.
-
-#pragma once
-
-#include "CoreMinimal.h"
-#include "EzAbilityNodeBase.h"
-#include "UObject/Object.h"
-#include "EzAbilityTransition.generated.h"
-
-/**
- * 
- */
-USTRUCT()
-struct EZABILITY_API FEzAbilityTransition : public FEzAbilityNodeBase
-{
-	GENERATED_BODY()
-};

+ 42 - 12
Ability/Plugins/EzAbility/Source/EzAbilityEditor/EzAbilityEditor.Build.cs

@@ -26,28 +26,58 @@ public class EzAbilityEditor : ModuleRules
 			new string[]
 			{
 				"Core",
-				// ... add other public dependencies that you statically link with here ...
-			}
-			);
-			
-		
-		PrivateDependencyModuleNames.AddRange(
-			new string[]
-			{
 				"CoreUObject",
 				"Engine",
+				"InputCore",
+				"AssetTools",
+				"EditorFramework",
+				"UnrealEd",
 				"Slate",
 				"SlateCore",
-				// ... add private dependencies that you statically link with here ...	
+				"EditorStyle",
+				"PropertyEditor",
+				"SourceControl",
+				"Projects",
+				"BlueprintGraph",
+				"PropertyAccessEditor",
+				"StructUtils",
+				"StructUtilsEngine",
+				"StructUtilsEditor",
+				"GameplayTags",
+				"EzAbility"
 			}
 			);
+			
 		
-		
-		DynamicallyLoadedModuleNames.AddRange(
+		PrivateDependencyModuleNames.AddRange(
 			new string[]
 			{
-				// ... add any modules that your module loads dynamically here ...
+				"AssetDefinition",
+				"RenderCore",
+				"GraphEditor",
+				"KismetWidgets",
+				"PropertyPath",
+				"ToolMenus",
+				"ToolWidgets",
+				"ApplicationCore",
+				"DeveloperSettings",
+				"RewindDebuggerInterface",
+				"DetailCustomizations",
+				"AppFramework"
 			}
 			);
+		
+		PrivateIncludePathModuleNames.AddRange(new string[] {
+			"MessageLog",
+		});
+		
+		if (Target.Platform == UnrealTargetPlatform.Win64)
+		{
+			PublicDefinitions.Add("WITH_EZABILITY_DEBUGGER=1");
+		}
+		else
+		{
+			PublicDefinitions.Add("WITH_EZABILITY_DEBUGGER=0");
+		}
 	}
 }

+ 2 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditor.cpp

@@ -2,6 +2,8 @@
 
 #include "EzAbilityEditor.h"
 
+DEFINE_LOG_CATEGORY(LogEzAbilityEditor);
+
 #define LOCTEXT_NAMESPACE "FEzAbilityEditorModule"
 
 void FEzAbilityEditorModule::StartupModule()

+ 1128 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditorData.cpp

@@ -2,3 +2,1131 @@
 
 
 #include "EzAbilityEditorData.h"
+
+#if WITH_EDITOR
+#include "Engine/UserDefinedStruct.h"
+#include "StructUtilsDelegates.h"
+#endif
+
+#include "EzAbilityDelegates.h"
+#include "EzAbilityEditor.h"
+#include "EzAbilityPropertyHelpers.h"
+#include "EzAbilitySchema.h"
+#include "Algo/LevenshteinDistance.h"
+#include "Condition/EzAbilityCondition.h"
+#include "Evaluator/EzAbilityEvaluator.h"
+#include "Task/EzAbilityTask.h"
+
+#include UE_INLINE_GENERATED_CPP_BY_NAME(EzAbilityEditorData)
+
+UEzAbilityEditorData::UEzAbilityEditorData()
+{
+	FEzAbilityEditorColor DefaultColor;
+	DefaultColor.ColorRef = FEzAbilityEditorColorRef();
+	DefaultColor.Color = FLinearColor(FColor(31, 151, 167));
+	DefaultColor.DisplayName = TEXT("Default Color");
+
+	Colors.Add(MoveTemp(DefaultColor));
+}
+
+void UEzAbilityEditorData::PostInitProperties()
+{
+	Super::PostInitProperties();
+
+	RootParameters.ID = FGuid::NewGuid();
+
+#if WITH_EDITOR
+	OnObjectsReinstancedHandle = FCoreUObjectDelegates::OnObjectsReinstanced.AddUObject(this, &UEzAbilityEditorData::OnObjectsReinstanced);
+	OnUserDefinedStructReinstancedHandle = UE::StructUtils::Delegates::OnUserDefinedStructReinstanced.AddUObject(this, &UEzAbilityEditorData::OnUserDefinedStructReinstanced);
+	OnParametersChangedHandle = UE::EzAbility::Delegates::OnParametersChanged.AddUObject(this, &UEzAbilityEditorData::OnParametersChanged);
+#endif
+}
+
+#if WITH_EDITOR
+
+void UEzAbilityEditorData::BeginDestroy()
+{
+	if (OnObjectsReinstancedHandle.IsValid())
+	{
+		FCoreUObjectDelegates::OnObjectsReinstanced.Remove(OnObjectsReinstancedHandle);
+		OnObjectsReinstancedHandle.Reset();
+	}
+	if (OnUserDefinedStructReinstancedHandle.IsValid())
+	{
+		UE::StructUtils::Delegates::OnUserDefinedStructReinstanced.Remove(OnUserDefinedStructReinstancedHandle);
+		OnUserDefinedStructReinstancedHandle.Reset();
+	}
+	if (OnParametersChangedHandle.IsValid())
+	{
+		UE::EzAbility::Delegates::OnParametersChanged.Remove(OnParametersChangedHandle);
+		OnParametersChangedHandle.Reset();
+	}
+	
+	Super::BeginDestroy();
+}
+
+void UEzAbilityEditorData::OnObjectsReinstanced(const FReplacementObjectMap& ObjectMap)
+{
+	if (ObjectMap.IsEmpty())
+	{
+		return;
+	}
+	
+	TSet<const UStruct*> Structs;
+	for (TMap<UObject*, UObject*>::TConstIterator It(ObjectMap); It; ++It)
+	{
+		if (const UObject* ObjectToBeReplaced = It->Value)
+		{
+			Structs.Add(ObjectToBeReplaced->GetClass());
+		}
+	}
+
+	bool bShouldUpdate = false;
+	VisitAllNodes([&Structs, &bShouldUpdate](const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+	{
+		if (Structs.Contains(Value.GetStruct()))
+		{
+			bShouldUpdate = true;
+			return EEzAbilityVisitor::Break; 
+		}
+		return EEzAbilityVisitor::Continue;
+	});
+
+	if (!bShouldUpdate)
+	{
+		bShouldUpdate = EditorBindings.ContainsAnyStruct(Structs);
+	}
+	
+	if (bShouldUpdate)
+	{
+		UpdateBindingsInstanceStructs();
+	}
+}
+
+void UEzAbilityEditorData::OnUserDefinedStructReinstanced(const UUserDefinedStruct& UserDefinedStruct)
+{
+	TSet<const UStruct*> Structs;
+	Structs.Add(&UserDefinedStruct);
+
+	bool bShouldUpdate = false;
+	VisitAllNodes([&Structs, &bShouldUpdate](const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+	{
+		if (Structs.Contains(Value.GetStruct()))
+		{
+			bShouldUpdate = true;
+			return EEzAbilityVisitor::Break; 
+		}
+		return EEzAbilityVisitor::Continue;
+	});
+
+	if (!bShouldUpdate)
+	{
+		bShouldUpdate = EditorBindings.ContainsAnyStruct(Structs);
+	}
+	
+	if (bShouldUpdate)
+	{
+		UpdateBindingsInstanceStructs();
+	}
+}
+
+void UEzAbilityEditorData::OnParametersChanged(const UEzAbility& EzAbility)
+{
+	if (const UEzAbility* OwnerEzAbility = GetTypedOuter<UEzAbility>())
+	{
+		if (OwnerEzAbility == &EzAbility)
+		{
+			UpdateBindingsInstanceStructs();
+		}
+	}
+}
+
+
+void UEzAbilityEditorData::PostLoad()
+{
+	Super::PostLoad();
+
+	// Ensure the schema and states have had their PostLoad() fixed applied as we may need them in the later calls (or EzAbility compile which might be calling this).
+	if (Schema)
+	{
+		Schema->ConditionalPostLoad();
+	}
+	VisitHierarchy([](UEzAbilityState& State, UEzAbilityState* ParentState) mutable 
+	{
+		State.ConditionalPostLoad();
+		return EEzAbilityVisitor::Continue;
+	});
+
+	ReparentStates();
+	FixObjectNodes();
+	FixDuplicateIDs();
+	UpdateBindingsInstanceStructs();
+}
+
+void UEzAbilityEditorData::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent)
+{
+	Super::PostEditChangeChainProperty(PropertyChangedEvent);
+
+	FProperty* Property = PropertyChangedEvent.Property;
+	FProperty* MemberProperty = nullptr;
+	if (PropertyChangedEvent.PropertyChain.GetActiveMemberNode())
+	{
+		MemberProperty = PropertyChangedEvent.PropertyChain.GetActiveMemberNode()->GetValue();
+	}
+
+	if (MemberProperty && Property)
+	{
+		const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+		checkf(EzAbility, TEXT("UEzAbilityEditorData should only be allocated within a UEzAbility"));
+		
+		const FName MemberName = MemberProperty->GetFName();
+		if (MemberName == GET_MEMBER_NAME_CHECKED(UEzAbilityEditorData, Schema))
+		{			
+			UE::EzAbility::Delegates::OnSchemaChanged.Broadcast(*EzAbility);
+		}
+		else if (MemberName == GET_MEMBER_NAME_CHECKED(UEzAbilityEditorData, RootParameters))
+		{
+			UE::EzAbility::Delegates::OnParametersChanged.Broadcast(*EzAbility);
+		}
+
+		// Ensure unique ID on duplicated items.
+		if (PropertyChangedEvent.ChangeType == EPropertyChangeType::Duplicate)
+		{
+			if (MemberName == GET_MEMBER_NAME_CHECKED(UEzAbilityEditorData, Evaluators))
+			{
+				const int32 ArrayIndex = PropertyChangedEvent.GetArrayIndex(MemberProperty->GetFName().ToString());
+				if (Evaluators.IsValidIndex(ArrayIndex))
+				{
+					if (FEzAbilityEvaluator* Eval = Evaluators[ArrayIndex].Node.GetMutablePtr<FEzAbilityEvaluator>())
+					{
+						Eval->Name = FName(Eval->Name.ToString() + TEXT(" Duplicate"));
+					}
+					
+					const FGuid OldStructID = Evaluators[ArrayIndex].ID;
+					Evaluators[ArrayIndex].ID = FGuid::NewGuid();
+					EditorBindings.CopyBindings(OldStructID, Evaluators[ArrayIndex].ID);
+				}
+			}
+			else if (MemberName == GET_MEMBER_NAME_CHECKED(UEzAbilityEditorData, GlobalTasks))
+			{
+				const int32 ArrayIndex = PropertyChangedEvent.GetArrayIndex(MemberProperty->GetFName().ToString());
+				if (GlobalTasks.IsValidIndex(ArrayIndex))
+				{
+					if (FEzAbilityTask* Task = GlobalTasks[ArrayIndex].Node.GetMutablePtr<FEzAbilityTask>())
+					{
+						Task->Name = FName(Task->Name.ToString() + TEXT(" Duplicate"));
+					}
+					
+					const FGuid OldStructID = GlobalTasks[ArrayIndex].ID;
+					GlobalTasks[ArrayIndex].ID = FGuid::NewGuid();
+					EditorBindings.CopyBindings(OldStructID, GlobalTasks[ArrayIndex].ID);
+				}
+			}
+		}
+		else if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ArrayRemove)
+		{
+			if (MemberProperty->GetFName() == GET_MEMBER_NAME_CHECKED(UEzAbilityEditorData, Evaluators)
+				|| MemberProperty->GetFName() == GET_MEMBER_NAME_CHECKED(UEzAbilityEditorData, GlobalTasks))
+			{
+				TMap<FGuid, const FEzAbilityDataView> AllStructValues;
+				GetAllStructValues(AllStructValues);
+				Modify();
+				EditorBindings.RemoveUnusedBindings(AllStructValues);
+			}
+		}
+
+		// Notify that the global data changed (will need to update binding widgets, etc)
+		if (MemberName == GET_MEMBER_NAME_CHECKED(UEzAbilityEditorData, Evaluators)
+			|| MemberName == GET_MEMBER_NAME_CHECKED(UEzAbilityEditorData, GlobalTasks))
+		{
+			UE::EzAbility::Delegates::OnGlobalDataChanged.Broadcast(*EzAbility);
+		}
+
+	}
+
+	UE::EzAbility::PropertyHelpers::DispatchPostEditToNodes(*this, PropertyChangedEvent);
+}
+#endif // WITH_EDITOR
+
+void UEzAbilityEditorData::GetAccessibleStructs(const FGuid TargetStructID, TArray<FEzAbilityBindableStructDesc>& OutStructDescs) const
+{
+	// Find the states that are updated before the current state.
+	TArray<const UEzAbilityState*> Path;
+	const UEzAbilityState* State = GetStateByStructID(TargetStructID);
+	while (State != nullptr)
+	{
+		Path.Insert(State, 0);
+
+		// Stop at subtree root.
+		if (State->Type == EEzAbilityStateType::Subtree)
+		{
+			break;
+		}
+
+		State = State->Parent;
+	}
+	
+	GetAccessibleStructs(Path, TargetStructID, OutStructDescs);
+}
+
+void UEzAbilityEditorData::GetAccessibleStructs(const TConstArrayView<const UEzAbilityState*> Path, const FGuid TargetStructID, TArray<FEzAbilityBindableStructDesc>& OutStructDescs) const
+{
+	const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+	checkf(EzAbility, TEXT("UEzAbilityEditorData should only be allocated within a UEzAbility"));
+
+
+	EEzAbilityVisitor BaseProgress = VisitGlobalNodes([&OutStructDescs, TargetStructID]
+		(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+	{
+		if (Desc.ID == TargetStructID)
+		{
+			return EEzAbilityVisitor::Break;
+		}
+		
+		OutStructDescs.Add(Desc);
+		
+		return EEzAbilityVisitor::Continue;
+	});
+
+
+	if (BaseProgress == EEzAbilityVisitor::Continue)
+	{
+		TArray<FEzAbilityBindableStructDesc> TaskDescs;
+
+		for (const UEzAbilityState* State : Path)
+		{
+			if (State == nullptr)
+			{
+				continue;
+			}
+			
+			const EEzAbilityVisitor StateProgress = VisitStateNodes(*State, [&OutStructDescs, &TaskDescs, TargetStructID]
+				(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+				{
+					// Stop iterating as soon as we find the target node.
+					if (Desc.ID == TargetStructID)
+					{
+						OutStructDescs.Append(TaskDescs);
+						return EEzAbilityVisitor::Break;
+					}
+
+					// Not at target yet, collect all bindable source accessible so far.
+					if (Desc.DataSource == EEzAbilityBindableStructSource::Task
+						|| Desc.DataSource == EEzAbilityBindableStructSource::State)
+					{
+						TaskDescs.Add(Desc);
+					}
+							
+					return EEzAbilityVisitor::Continue;
+				});
+			
+			if (StateProgress == EEzAbilityVisitor::Break)
+			{
+				break;
+			}
+		}
+	}
+	
+	OutStructDescs.StableSort([](const FEzAbilityBindableStructDesc& A, const FEzAbilityBindableStructDesc& B)
+	{
+		return (uint8)A.DataSource < (uint8)B.DataSource;
+	});
+}
+
+FEzAbilityBindableStructDesc UEzAbilityEditorData::FindContextData(const UStruct* ObjectType, const FString ObjectNameHint) const
+{
+	if (Schema == nullptr)
+	{
+		return FEzAbilityBindableStructDesc();
+	}
+
+	// Find candidates based on type.
+	TArray<FEzAbilityBindableStructDesc> Candidates;
+	for (const FEzAbilityExternalDataDesc& Desc : Schema->GetContextDataDescs())
+	{
+		if (Desc.Struct->IsChildOf(ObjectType))
+		{
+			Candidates.Emplace(Desc.Name, Desc.Struct, FEzAbilityDataHandle(), EEzAbilityBindableStructSource::Context, Desc.ID);
+		}
+	}
+
+	// Handle trivial cases.
+	if (Candidates.IsEmpty())
+	{
+		return FEzAbilityBindableStructDesc();
+	}
+
+	if (Candidates.Num() == 1)
+	{
+		return Candidates[0];
+	}
+	
+	check(!Candidates.IsEmpty());
+	
+	// Multiple candidates, pick one that is closest match based on name.
+	auto CalculateScore = [](const FString& Name, const FString& CandidateName)
+	{
+		if (CandidateName.IsEmpty())
+		{
+			return 1.0f;
+		}
+		const float WorstCase = static_cast<float>(Name.Len() + CandidateName.Len());
+		return 1.0f - (Algo::LevenshteinDistance(Name, CandidateName) / WorstCase);
+	};
+	
+	const FString ObjectNameLowerCase = ObjectNameHint.ToLower();
+	
+	int32 HighestScoreIndex = 0;
+	float HighestScore = CalculateScore(ObjectNameLowerCase, Candidates[0].Name.ToString().ToLower());
+	
+	for (int32 Index = 1; Index < Candidates.Num(); Index++)
+	{
+		const float Score = CalculateScore(ObjectNameLowerCase, Candidates[Index].Name.ToString().ToLower());
+		if (Score > HighestScore)
+		{
+			HighestScore = Score;
+			HighestScoreIndex = Index;
+		}
+	}
+	
+	return Candidates[HighestScoreIndex];
+}
+
+bool UEzAbilityEditorData::GetStructByID(const FGuid StructID, FEzAbilityBindableStructDesc& OutStructDesc) const
+{
+	VisitAllNodes([&OutStructDesc, StructID](const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+	{
+		if (Desc.ID == StructID)
+		{
+			OutStructDesc = Desc;
+			return EEzAbilityVisitor::Break;
+		}
+		return EEzAbilityVisitor::Continue;
+	});
+	
+	return OutStructDesc.IsValid();
+}
+
+bool UEzAbilityEditorData::GetDataViewByID(const FGuid StructID, FEzAbilityDataView& OutDataView) const
+{
+	bool bFound = false;
+	VisitAllNodes([&OutDataView, &bFound, StructID](const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+	{
+		if (Desc.ID == StructID)
+		{
+			bFound = true;
+			OutDataView = Value;
+			return EEzAbilityVisitor::Break;
+		}
+		return EEzAbilityVisitor::Continue;
+	});
+
+	return bFound;
+}
+
+const UEzAbilityState* UEzAbilityEditorData::GetStateByStructID(const FGuid TargetStructID) const
+{
+	const UEzAbilityState* Result = nullptr;
+
+	VisitHierarchyNodes([&Result, TargetStructID](const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+		{
+			if (Desc.ID == TargetStructID)
+			{
+				Result = State;
+				return EEzAbilityVisitor::Break;
+			}
+			return EEzAbilityVisitor::Continue;
+			
+		});
+
+	return Result;
+}
+
+const UEzAbilityState* UEzAbilityEditorData::GetStateByID(const FGuid StateID) const
+{
+	return const_cast<UEzAbilityEditorData*>(this)->GetMutableStateByID(StateID);
+}
+
+UEzAbilityState* UEzAbilityEditorData::GetMutableStateByID(const FGuid StateID)
+{
+	UEzAbilityState* Result = nullptr;
+	
+	VisitHierarchy([&Result, &StateID](UEzAbilityState& State, UEzAbilityState* /*ParentState*/)
+	{
+		if (State.ID == StateID)
+		{
+			Result = &State;
+			return EEzAbilityVisitor::Break;
+		}
+
+		return EEzAbilityVisitor::Continue;
+	});
+
+	return Result;
+}
+
+void UEzAbilityEditorData::GetAllStructIDs(TMap<FGuid, const UStruct*>& AllStructs) const
+{
+	AllStructs.Reset();
+
+	const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+	checkf(EzAbility, TEXT("UEzAbilityEditorData should only be allocated within a UEzAbility"));
+
+	VisitAllNodes([&AllStructs](const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+		{
+			AllStructs.Emplace(Desc.ID, Desc.Struct);
+			return EEzAbilityVisitor::Continue;
+		});
+}
+
+void UEzAbilityEditorData::GetAllStructValues(TMap<FGuid, const FEzAbilityDataView>& AllValues) const
+{
+	AllValues.Reset();
+
+	const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+	checkf(EzAbility, TEXT("UEzAbilityEditorData should only be allocated within a UEzAbility"));
+
+	VisitAllNodes([&AllValues](const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)
+		{
+			AllValues.Emplace(Desc.ID, Value);
+			return EEzAbilityVisitor::Continue;
+		});
+}
+
+void UEzAbilityEditorData::ReparentStates()
+{
+	VisitHierarchy([TreeData = this](UEzAbilityState& State, UEzAbilityState* ParentState) mutable 
+	{
+		UObject* ExpectedOuter = ParentState ? Cast<UObject>(ParentState) : Cast<UObject>(TreeData);
+		if (State.GetOuter() != ExpectedOuter)
+		{
+			UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Fixing outer on state %s."), *TreeData->GetFullName(), *GetNameSafe(&State));
+			State.Rename(nullptr, ExpectedOuter, REN_DontCreateRedirectors | REN_DoNotDirty | REN_ForceNoResetLoaders);
+		}
+		
+		State.Parent = ParentState;
+		
+		return EEzAbilityVisitor::Continue;
+	});
+}
+
+void UEzAbilityEditorData::FixObjectInstance(TSet<UObject*>& SeenObjects, UObject& Outer, FEzAbilityEditorNode& Node)
+{
+	if (Node.InstanceObject)
+	{
+		// Found a duplicate reference to an object, make unique copy.
+		if (SeenObjects.Contains(Node.InstanceObject))
+		{
+			UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Making duplicate node instance %s unique."), *GetFullName(), *GetNameSafe(Node.InstanceObject));
+			Node.InstanceObject = DuplicateObject(Node.InstanceObject, &Outer);
+		}
+		else
+		{
+			// Make sure the instance object is property outered.
+			if (Node.InstanceObject->GetOuter() != &Outer)
+			{
+				UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Fixing outer on node instance %s."), *GetFullName(), *GetNameSafe(Node.InstanceObject));
+				Node.InstanceObject->Rename(nullptr, &Outer, REN_DontCreateRedirectors | REN_DoNotDirty | REN_ForceNoResetLoaders);
+			}
+		}
+		SeenObjects.Add(Node.InstanceObject);
+	}
+};
+
+void UEzAbilityEditorData::FixObjectNodes()
+{
+	// Older version of State Trees had all instances outered to the editor data. This causes issues with State copy/paste.
+	// Instance data does not get duplicated but the copied state will reference the object on the source state instead.
+	//
+	// Ensure that all node objects are parented to their states, and make duplicated instances unique.
+
+	TSet<UObject*> SeenObjects;
+	
+	VisitHierarchy([&SeenObjects, TreeData = this](UEzAbilityState& State, UEzAbilityState* ParentState) mutable 
+	{
+
+		// Enter conditions
+		for (FEzAbilityEditorNode& Node : State.EnterConditions)
+		{
+			TreeData->FixObjectInstance(SeenObjects, State, Node);
+		}
+		
+		// Tasks
+		for (FEzAbilityEditorNode& Node : State.Tasks)
+		{
+			TreeData->FixObjectInstance(SeenObjects, State, Node);
+		}
+
+		TreeData->FixObjectInstance(SeenObjects, State, State.SingleTask);
+
+
+		// Transitions
+		for (FEzAbilityTransition& Transition : State.Transitions)
+		{
+			for (FEzAbilityEditorNode& Node : Transition.Conditions)
+			{
+				TreeData->FixObjectInstance(SeenObjects, State, Node);
+			}
+		}
+		
+		return EEzAbilityVisitor::Continue;
+	});
+
+	for (FEzAbilityEditorNode& Node : Evaluators)
+	{
+		FixObjectInstance(SeenObjects, *this, Node);
+	}
+
+	for (FEzAbilityEditorNode& Node : GlobalTasks)
+	{
+		FixObjectInstance(SeenObjects, *this, Node);
+	}
+}
+
+void UEzAbilityEditorData::FixDuplicateIDs()
+{
+	// Around version 5.1-5.3 we had issue that copy/paste or some duplication methods could create nodes with duplicate IDs.
+	// This code tries to fix that, it looks for duplicates, makes them unique, and duplicates the bindings when ID changes.
+	TSet<FGuid> FoundNodeIDs;
+
+	// Evaluators
+	for (int32 Index = 0; Index < Evaluators.Num(); Index++)
+	{
+		FEzAbilityEditorNode& Node = Evaluators[Index];
+		if (const FEzAbilityEvaluator* Evaluator = Node.Node.GetPtr<FEzAbilityEvaluator>())
+		{
+			const FGuid OldID = Node.ID; 
+			if (FoundNodeIDs.Contains(Node.ID))
+			{
+				Node.ID = UE::EzAbility::PropertyHelpers::MakeDeterministicID(*this, TEXT("Evaluators"), Index);
+				
+				UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Found Evaluator '%s' with duplicate ID, changing ID:%s to ID:%s."),
+					*GetFullName(), *Node.GetName().ToString(), *OldID.ToString(), *Node.ID.ToString());
+				EditorBindings.CopyBindings(OldID, Node.ID);
+			}
+			FoundNodeIDs.Add(Node.ID);
+		}
+	}
+	
+	// Global Tasks
+	for (int32 Index = 0; Index < GlobalTasks.Num(); Index++)
+	{
+		FEzAbilityEditorNode& Node = GlobalTasks[Index];
+		if (const FEzAbilityTask* Task = Node.Node.GetPtr<FEzAbilityTask>())
+		{
+			const FGuid OldID = Node.ID; 
+			if (FoundNodeIDs.Contains(Node.ID))
+			{
+				Node.ID = UE::EzAbility::PropertyHelpers::MakeDeterministicID(*this, TEXT("GlobalTasks"), Index);
+				
+				UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Found GlobalTask '%s' with duplicate ID, changing ID:%s to ID:%s."),
+					*GetFullName(), *Node.GetName().ToString(), *OldID.ToString(), *Node.ID.ToString());
+				EditorBindings.CopyBindings(OldID, Node.ID);
+			}
+			FoundNodeIDs.Add(Node.ID);
+		}
+	}
+	
+	VisitHierarchy([&FoundNodeIDs, &EditorBindings = EditorBindings, &Self = *this](UEzAbilityState& State, UEzAbilityState* ParentState)
+	{
+		// Enter conditions
+		for (int32 Index = 0; Index < State.EnterConditions.Num(); Index++)
+		{
+			FEzAbilityEditorNode& Node = State.EnterConditions[Index];
+			if (const FEzAbilityCondition* Cond = Node.Node.GetPtr<FEzAbilityCondition>())
+			{
+				const FGuid OldID = Node.ID;
+				
+				bool bIsAlreadyInSet = false;
+				FoundNodeIDs.Add(Node.ID, &bIsAlreadyInSet);
+				if (bIsAlreadyInSet)
+				{
+					Node.ID = UE::EzAbility::PropertyHelpers::MakeDeterministicID(State, TEXT("EnterConditions"), Index);
+					
+					UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Found Enter Condition '%s' with duplicate ID on state '%s', changing ID:%s to ID:%s."),
+						*Self.GetFullName(), *Node.GetName().ToString(), *GetNameSafe(&State), *OldID.ToString(), *Node.ID.ToString());
+					EditorBindings.CopyBindings(OldID, Node.ID);
+				}
+			}
+		}
+
+		// Tasks
+		for (int32 Index = 0; Index < State.Tasks.Num(); Index++)
+		{
+			FEzAbilityEditorNode& Node = State.Tasks[Index];
+			if (const FEzAbilityTask* Task = Node.Node.GetPtr<FEzAbilityTask>())
+			{
+				const FGuid OldID = Node.ID;
+				
+				bool bIsAlreadyInSet = false;
+				FoundNodeIDs.Add(Node.ID, &bIsAlreadyInSet);
+				if (bIsAlreadyInSet)
+				{
+					Node.ID = UE::EzAbility::PropertyHelpers::MakeDeterministicID(State, TEXT("Tasks"), Index);
+
+					UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Found Task '%s' with duplicate ID on state '%s', changing ID:%s to ID:%s."),
+						*Self.GetFullName(), *Node.GetName().ToString(), *GetNameSafe(&State), *OldID.ToString(), *Node.ID.ToString());
+					EditorBindings.CopyBindings(OldID, Node.ID);
+				}
+			}
+		}
+
+		if (FEzAbilityTask* Task = State.SingleTask.Node.GetMutablePtr<FEzAbilityTask>())
+		{
+			const FGuid OldID = State.SingleTask.ID;
+
+			bool bIsAlreadyInSet = false;
+			FoundNodeIDs.Add(State.SingleTask.ID, &bIsAlreadyInSet);
+			if (bIsAlreadyInSet)
+			{
+				State.SingleTask.ID = UE::EzAbility::PropertyHelpers::MakeDeterministicID(State, TEXT("SingleTask"), 0);
+
+				UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Found enter condition '%s' with duplicate ID on state '%s', changing ID:%s to ID:%s."),
+					*Self.GetFullName(), *State.SingleTask.GetName().ToString(), *GetNameSafe(&State), *OldID.ToString(), *State.SingleTask.ID.ToString());
+				EditorBindings.CopyBindings(OldID, State.SingleTask.ID);
+			}
+		}
+
+		// Transitions
+		for (int32 TransitionIndex = 0; TransitionIndex < State.Transitions.Num(); TransitionIndex++)
+		{
+			FEzAbilityTransition& Transition = State.Transitions[TransitionIndex];
+			for (int32 Index = 0; Index < Transition.Conditions.Num(); Index++)
+			{
+				FEzAbilityEditorNode& Node = Transition.Conditions[Index];
+				if (const FEzAbilityCondition* Cond = Node.Node.GetPtr<FEzAbilityCondition>())
+				{
+					const FGuid OldID = Node.ID; 
+					bool bIsAlreadyInSet = false;
+					FoundNodeIDs.Add(Node.ID, &bIsAlreadyInSet);
+					if (bIsAlreadyInSet)
+					{
+						Node.ID = UE::EzAbility::PropertyHelpers::MakeDeterministicID(State, TEXT("TransitionConditions"), ((uint64)TransitionIndex << 32) | (uint64)Index);
+
+						UE_LOG(LogEzAbilityEditor, Log, TEXT("%s: Found transition condition '%s' with duplicate ID on state '%s', changing ID:%s to ID:%s."),
+							*Self.GetFullName(), *Node.GetName().ToString(), *GetNameSafe(&State), *OldID.ToString(), *Node.ID.ToString());
+						EditorBindings.CopyBindings(OldID, Node.ID);
+					}
+				}
+			}
+		}
+		
+		return EEzAbilityVisitor::Continue;
+	});
+
+	// It is possible that the user has changed the node type so some of the bindings might not make sense anymore, clean them up.
+	TMap<FGuid, const FEzAbilityDataView> AllValues;
+	GetAllStructValues(AllValues);
+	EditorBindings.RemoveUnusedBindings(AllValues);
+}
+
+void UEzAbilityEditorData::UpdateBindingsInstanceStructs()
+{
+	TMap<FGuid, const FEzAbilityDataView> AllValues;
+	GetAllStructValues(AllValues);
+	for (FEzAbilityPropertyPathBinding& Binding : EditorBindings.GetMutableBindings())
+	{
+		if (AllValues.Contains(Binding.GetSourcePath().GetStructID()))
+		{
+			Binding.GetMutableSourcePath().UpdateSegmentsFromValue(AllValues[Binding.GetSourcePath().GetStructID()]);
+		}
+
+		if (AllValues.Contains(Binding.GetTargetPath().GetStructID()))
+		{
+			Binding.GetMutableTargetPath().UpdateSegmentsFromValue(AllValues[Binding.GetTargetPath().GetStructID()]);
+		}
+	}
+}
+
+EEzAbilityVisitor UEzAbilityEditorData::VisitStateNodes(const UEzAbilityState& State, TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FGuid& ID, const FName& Name, const EEzAbilityNodeType NodeType, const UScriptStruct* NodeStruct, const UStruct* InstanceStruct)> InFunc) const
+{
+	bool bContinue = true;
+
+	if (bContinue)
+	{
+		// Enter conditions
+		for (const FEzAbilityEditorNode& Node : State.EnterConditions)
+		{
+			if (const FEzAbilityCondition* Cond = Node.Node.GetPtr<FEzAbilityCondition>())
+			{
+				if (InFunc(&State, Node.ID, Node.Node.GetScriptStruct()->GetFName(), EEzAbilityNodeType::EnterCondition, Node.Node.GetScriptStruct(), Cond->GetInstanceDataType()) == EEzAbilityVisitor::Break)
+				{
+					bContinue = false;
+					break;
+				}
+			}
+		}
+	}
+	if (bContinue)
+	{
+		// Tasks
+		for (const FEzAbilityEditorNode& Node : State.Tasks)
+		{
+			if (const FEzAbilityTask* Task = Node.Node.GetPtr<FEzAbilityTask>())
+			{
+				if (InFunc(&State, Node.ID, Task->Name, EEzAbilityNodeType::Task, Node.Node.GetScriptStruct(), Task->GetInstanceDataType()) == EEzAbilityVisitor::Break)
+				{
+					bContinue = false;
+					break;
+				}
+			}
+		}
+	}
+	if (bContinue)
+	{
+		if (const FEzAbilityTask* Task = State.SingleTask.Node.GetPtr<FEzAbilityTask>())
+		{
+			if (InFunc(&State, State.SingleTask.ID, Task->Name, EEzAbilityNodeType::Task, State.SingleTask.Node.GetScriptStruct(), Task->GetInstanceDataType()) == EEzAbilityVisitor::Break)
+			{
+				bContinue = false;
+			}
+		}
+
+	}
+	if (bContinue)
+	{
+		// Transitions
+		for (const FEzAbilityTransition& Transition : State.Transitions)
+		{
+			for (const FEzAbilityEditorNode& Node : Transition.Conditions)
+			{
+				if (const FEzAbilityCondition* Cond = Node.Node.GetPtr<FEzAbilityCondition>())
+				{
+					if (InFunc(&State, Node.ID, Node.Node.GetScriptStruct()->GetFName(), EEzAbilityNodeType::TransitionCondition, Node.Node.GetScriptStruct(), Cond->GetInstanceDataType()) == EEzAbilityVisitor::Break)
+					{
+						bContinue = false;
+						break;
+					}
+				}
+			}
+		}
+	}
+	if (bContinue)
+	{
+		// Bindable state parameters
+		if (State.Type != EEzAbilityStateType::Subtree
+			&& State.Parameters.Parameters.IsValid())
+		{
+			if (InFunc(&State, State.Parameters.ID, State.Name, EEzAbilityNodeType::StateParameters, nullptr, State.Parameters.Parameters.GetPropertyBagStruct()) == EEzAbilityVisitor::Break)
+			{
+				bContinue = false;
+			}
+		}
+	}
+
+	return bContinue ? EEzAbilityVisitor::Continue : EEzAbilityVisitor::Break;
+}
+
+
+EEzAbilityVisitor UEzAbilityEditorData::VisitStateNodes(const UEzAbilityState& State, TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)> InFunc) const
+{
+	bool bContinue = true;
+
+	if (bContinue)
+	{
+		// Bindable state parameters
+		if (State.Parameters.Parameters.IsValid())
+		{
+			FEzAbilityBindableStructDesc Desc;
+			Desc.Struct = State.Parameters.Parameters.GetPropertyBagStruct();
+			Desc.Name = State.Name;
+			Desc.ID = State.Parameters.ID;
+			Desc.DataSource = EEzAbilityBindableStructSource::State;
+
+			if (InFunc(&State, Desc, FEzAbilityDataView(const_cast<FInstancedPropertyBag&>(State.Parameters.Parameters).GetMutableValue())) == EEzAbilityVisitor::Break)
+			{
+				bContinue = false;
+			}
+		}
+	}
+	
+	if (bContinue)
+	{
+		// Enter conditions
+		for (const FEzAbilityEditorNode& Node : State.EnterConditions)
+		{
+			if (const FEzAbilityCondition* Cond = Node.Node.GetPtr<FEzAbilityCondition>())
+			{
+				FEzAbilityBindableStructDesc Desc;
+				Desc.Struct = Cond->GetInstanceDataType();
+				Desc.Name = Cond->Name;
+				Desc.ID = Node.ID;
+				Desc.DataSource = EEzAbilityBindableStructSource::Condition;
+
+				if (InFunc(&State, Desc, Node.GetInstance()) == EEzAbilityVisitor::Break)
+				{
+					bContinue = false;
+					break;
+				}
+			}
+		}
+	}
+	if (bContinue)
+	{
+		// Tasks
+		for (const FEzAbilityEditorNode& Node : State.Tasks)
+		{
+			if (const FEzAbilityTask* Task = Node.Node.GetPtr<FEzAbilityTask>())
+			{
+				FEzAbilityBindableStructDesc Desc;
+				Desc.Struct = Task->GetInstanceDataType();
+				Desc.Name = Task->Name;
+				Desc.ID = Node.ID;
+				Desc.DataSource = EEzAbilityBindableStructSource::Task;
+
+				if (InFunc(&State, Desc, Node.GetInstance()) == EEzAbilityVisitor::Break)
+				{
+					bContinue = false;
+					break;
+				}
+			}
+		}
+	}
+	if (bContinue)
+	{
+		if (const FEzAbilityTask* Task = State.SingleTask.Node.GetPtr<FEzAbilityTask>())
+		{
+			FEzAbilityBindableStructDesc Desc;
+			Desc.Struct = Task->GetInstanceDataType();
+			Desc.Name = Task->Name;
+			Desc.ID = State.SingleTask.ID;
+			Desc.DataSource = EEzAbilityBindableStructSource::Task;
+
+			if (InFunc(&State, Desc, State.SingleTask.GetInstance()) == EEzAbilityVisitor::Break)
+			{
+				bContinue = false;
+			}
+		}
+
+	}
+	if (bContinue)
+	{
+		// Transitions
+		for (const FEzAbilityTransition& Transition : State.Transitions)
+		{
+			for (const FEzAbilityEditorNode& Node : Transition.Conditions)
+			{
+				if (const FEzAbilityCondition* Cond = Node.Node.GetPtr<FEzAbilityCondition>())
+				{
+					FEzAbilityBindableStructDesc Desc;
+					Desc.Struct = Cond->GetInstanceDataType();
+					Desc.Name = Cond->Name;
+					Desc.ID = Node.ID;
+					Desc.DataSource = EEzAbilityBindableStructSource::Condition;
+
+					if (InFunc(&State, Desc, Node.GetInstance()) == EEzAbilityVisitor::Break)
+					{
+						bContinue = false;
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	return bContinue ? EEzAbilityVisitor::Continue : EEzAbilityVisitor::Break;
+}
+
+
+EEzAbilityVisitor UEzAbilityEditorData::VisitHierarchy(TFunctionRef<EEzAbilityVisitor(UEzAbilityState& State, UEzAbilityState* ParentState)> InFunc) const
+{
+	using FStatePair = TTuple<UEzAbilityState*, UEzAbilityState*>; 
+	TArray<FStatePair> Stack;
+	bool bContinue = true;
+
+	for (UEzAbilityState* SubTree : SubTrees)
+	{
+		if (!SubTree)
+		{
+			continue;
+		}
+
+		Stack.Add( FStatePair(nullptr, SubTree));
+
+		while (!Stack.IsEmpty() && bContinue)
+		{
+			FStatePair Current = Stack[0];
+			UEzAbilityState* ParentState = Current.Get<0>();
+			UEzAbilityState* State = Current.Get<1>();
+			check(State);
+
+			Stack.RemoveAt(0);
+
+			bContinue = InFunc(*State, ParentState) == EEzAbilityVisitor::Continue;
+			
+			if (bContinue)
+			{
+				// Children
+				for (UEzAbilityState* ChildState : State->Children)
+				{
+					Stack.Add(FStatePair(State, ChildState));
+				}
+			}
+		}
+		
+		if (!bContinue)
+		{
+			break;
+		}
+	}
+
+	return EEzAbilityVisitor::Continue;
+}
+
+void UEzAbilityEditorData::VisitHierarchyNodes(TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FGuid& ID, const FName& Name, const EEzAbilityNodeType NodeType, const UScriptStruct* NodeStruct, const UStruct* InstanceStruct)> InFunc) const
+{
+	PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	VisitHierarchy([this, &InFunc](const UEzAbilityState& State, UEzAbilityState* /*ParentState*/)
+	{
+		return VisitStateNodes(State, InFunc);
+	});
+	PRAGMA_ENABLE_DEPRECATION_WARNINGS
+}
+
+EEzAbilityVisitor UEzAbilityEditorData::VisitGlobalNodes(TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)> InFunc) const
+{
+	// Root parameters
+	{
+		FEzAbilityBindableStructDesc Desc;
+		Desc.Struct = RootParameters.Parameters.GetPropertyBagStruct();
+		Desc.Name = FName(TEXT("Parameters"));
+		Desc.ID = RootParameters.ID;
+		Desc.DataSource = EEzAbilityBindableStructSource::Parameter;
+		
+		if (InFunc(nullptr, Desc, FEzAbilityDataView(const_cast<FInstancedPropertyBag&>(RootParameters.Parameters).GetMutableValue())) == EEzAbilityVisitor::Break)
+		{
+			return EEzAbilityVisitor::Break;
+		}
+	}
+
+	// All named external data items declared by the schema
+	if (Schema != nullptr)
+	{
+		for (const FEzAbilityExternalDataDesc& ContextDesc : Schema->GetContextDataDescs())
+		{
+			FEzAbilityBindableStructDesc Desc;
+			Desc.Struct = ContextDesc.Struct;
+			Desc.Name = ContextDesc.Name;
+			Desc.ID = ContextDesc.ID;
+			Desc.DataSource = EEzAbilityBindableStructSource::Context;
+
+			// We don't have value for the external objects, but return the type and null value so that users of GetAllStructValues() can use the type. 
+			if (InFunc(nullptr, Desc, FEzAbilityDataView(Desc.Struct, nullptr)) == EEzAbilityVisitor::Break)
+			{
+				return EEzAbilityVisitor::Break;
+			}
+		}
+	}
+
+	// Evaluators
+	for (const FEzAbilityEditorNode& Node : Evaluators)
+	{
+		if (const FEzAbilityEvaluator* Evaluator = Node.Node.GetPtr<FEzAbilityEvaluator>())
+		{
+			FEzAbilityBindableStructDesc Desc;
+			Desc.Struct = Evaluator->GetInstanceDataType();
+			Desc.Name = Evaluator->Name;
+			Desc.ID = Node.ID;
+			Desc.DataSource = EEzAbilityBindableStructSource::Evaluator;
+
+			if (InFunc(nullptr, Desc, Node.GetInstance()) == EEzAbilityVisitor::Break)
+			{
+				return EEzAbilityVisitor::Break;
+			}
+		}
+	}
+
+	// Global tasks
+	for (const FEzAbilityEditorNode& Node : GlobalTasks)
+	{
+		if (const FEzAbilityTask* Task = Node.Node.GetPtr<FEzAbilityTask>())
+		{
+			FEzAbilityBindableStructDesc Desc;
+			Desc.Struct = Task->GetInstanceDataType();
+			Desc.Name = Task->Name;
+			Desc.ID = Node.ID;
+			Desc.DataSource = EEzAbilityBindableStructSource::GlobalTask;
+
+			if (InFunc(nullptr, Desc, Node.GetInstance()) == EEzAbilityVisitor::Break)
+			{
+				return EEzAbilityVisitor::Break;
+			}
+		}
+	}
+
+	return  EEzAbilityVisitor::Continue;
+}
+
+EEzAbilityVisitor UEzAbilityEditorData::VisitHierarchyNodes(TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)> InFunc) const
+{
+	return VisitHierarchy([this, &InFunc](const UEzAbilityState& State, UEzAbilityState* /*ParentState*/)
+	{
+		return VisitStateNodes(State, InFunc);
+	});
+}
+
+EEzAbilityVisitor UEzAbilityEditorData::VisitAllNodes(TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)> InFunc) const
+{
+	if (VisitGlobalNodes(InFunc) == EEzAbilityVisitor::Break)
+	{
+		return EEzAbilityVisitor::Break;
+	}
+
+	return VisitHierarchyNodes(InFunc);
+}
+
+#if WITH_EZABILITY_DEBUGGER
+bool UEzAbilityEditorData::HasAnyBreakpoint(const FGuid ID) const
+{
+	return Breakpoints.ContainsByPredicate([ID](const FEzAbilityEditorBreakpoint& Breakpoint) { return Breakpoint.ID == ID; });
+}
+
+bool UEzAbilityEditorData::HasBreakpoint(const FGuid ID, const EEzAbilityBreakpointType BreakpointType) const
+{
+	return GetBreakpoint(ID, BreakpointType) != nullptr;
+}
+
+const FEzAbilityEditorBreakpoint* UEzAbilityEditorData::GetBreakpoint(const FGuid ID, const EEzAbilityBreakpointType BreakpointType) const
+{
+	return Breakpoints.FindByPredicate([ID, BreakpointType](const FEzAbilityEditorBreakpoint& Breakpoint)
+		{
+			return Breakpoint.ID == ID && Breakpoint.BreakpointType == BreakpointType;
+		});
+}
+
+void UEzAbilityEditorData::AddBreakpoint(const FGuid ID, const EEzAbilityBreakpointType BreakpointType)
+{
+	Breakpoints.Emplace(ID, BreakpointType);
+
+	const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+	checkf(EzAbility, TEXT("UEzAbilityEditorData should only be allocated within a UEzAbility"));
+	UE::EzAbility::Delegates::OnBreakpointsChanged.Broadcast(*EzAbility);
+}
+
+bool UEzAbilityEditorData::RemoveBreakpoint(const FGuid ID, const EEzAbilityBreakpointType BreakpointType)
+{
+	const int32 Index = Breakpoints.IndexOfByPredicate([ID, BreakpointType](const FEzAbilityEditorBreakpoint& Breakpoint)
+		{
+			return Breakpoint.ID == ID && Breakpoint.BreakpointType == BreakpointType;
+		});
+		
+	if (Index != INDEX_NONE)
+	{
+		Breakpoints.RemoveAtSwap(Index);
+		
+		const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+		checkf(EzAbility, TEXT("UEzAbilityEditorData should only be allocated within a UEzAbility"));
+		UE::EzAbility::Delegates::OnBreakpointsChanged.Broadcast(*EzAbility);
+	}
+
+	return Index != INDEX_NONE;
+}
+
+#endif // WITH_EZABILITY_DEBUGGER
+
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+void UEzAbilityEditorData::AddPropertyBinding(const FEzAbilityEditorPropertyPath& SourcePath, const FEzAbilityEditorPropertyPath& TargetPath)
+{
+	EditorBindings.AddPropertyBinding(UE::EzAbility::Private::ConvertEditorPath(SourcePath), UE::EzAbility::Private::ConvertEditorPath(TargetPath));
+}
+PRAGMA_ENABLE_DEPRECATION_WARNINGS

+ 4 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditorNode.cpp

@@ -0,0 +1,4 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "EzAbilityEditorNode.h"

+ 204 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditorPropertyBindings.cpp

@@ -2,3 +2,207 @@
 
 
 #include "EzAbilityEditorPropertyBindings.h"
+
+#include "EzAbilityLog.h"
+
+#include UE_INLINE_GENERATED_CPP_BY_NAME(EzAbilityEditorPropertyBindings)
+
+UEzAbilityEditorPropertyBindingsOwner::UEzAbilityEditorPropertyBindingsOwner(const FObjectInitializer& ObjectInitializer)
+	: Super(ObjectInitializer)
+{
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+void FEzAbilityEditorPropertyBindings::AddPropertyBinding(const FEzAbilityPropertyPath& SourcePath, const FEzAbilityPropertyPath& TargetPath)
+{
+	RemovePropertyBindings(TargetPath);
+	PropertyBindings.Add(FEzAbilityPropertyPathBinding(SourcePath, TargetPath));
+}
+
+void FEzAbilityEditorPropertyBindings::RemovePropertyBindings(const FEzAbilityPropertyPath& TargetPath)
+{
+	PropertyBindings.RemoveAll([&TargetPath](const FEzAbilityPropertyPathBinding& Binding)
+		{
+			return Binding.GetTargetPath() == TargetPath;
+		});
+}
+
+void FEzAbilityEditorPropertyBindings::CopyBindings(const FGuid FromStructID, const FGuid ToStructID)
+{
+	// Copy all bindings that target "FromStructID" and retarget them to "ToStructID".
+	TArray<FEzAbilityPropertyPathBinding> NewBindings;
+	for (const FEzAbilityPropertyPathBinding& Binding : PropertyBindings)
+	{
+		if (Binding.GetTargetPath().GetStructID() == FromStructID)
+		{
+			NewBindings.Emplace(Binding.GetSourcePath(), FEzAbilityPropertyPath(ToStructID, Binding.GetTargetPath().GetSegments()));
+		}
+	}
+
+	PropertyBindings.Append(NewBindings);
+}
+
+bool FEzAbilityEditorPropertyBindings::HasPropertyBinding(const FEzAbilityPropertyPath& TargetPath) const
+{
+	return PropertyBindings.ContainsByPredicate([&TargetPath](const FEzAbilityPropertyPathBinding& Binding)
+		{
+			return Binding.GetTargetPath() == TargetPath;
+		});
+}
+
+const FEzAbilityPropertyPath* FEzAbilityEditorPropertyBindings::GetPropertyBindingSource(const FEzAbilityPropertyPath& TargetPath) const
+{
+	const FEzAbilityPropertyPathBinding* Binding = PropertyBindings.FindByPredicate([&TargetPath](const FEzAbilityPropertyPathBinding& Binding)
+		{
+			return Binding.GetTargetPath() == TargetPath;
+		});
+	return Binding ? &Binding->GetSourcePath() : nullptr;
+}
+
+void FEzAbilityEditorPropertyBindings::GetPropertyBindingsFor(const FGuid StructID, TArray<FEzAbilityPropertyPathBinding>& OutBindings) const
+{
+	OutBindings = PropertyBindings.FilterByPredicate([StructID](const FEzAbilityPropertyPathBinding& Binding)
+		{
+			return Binding.GetSourcePath().GetStructID().IsValid()
+				&& Binding.GetTargetPath().GetStructID() == StructID;
+		});
+}
+
+void FEzAbilityEditorPropertyBindings::RemoveUnusedBindings(const TMap<FGuid, const FEzAbilityDataView>& ValidStructs)
+{
+	PropertyBindings.RemoveAll([ValidStructs](FEzAbilityPropertyPathBinding& Binding)
+		{
+			// Remove binding if it's target struct has been removed
+			if (!ValidStructs.Contains(Binding.GetTargetPath().GetStructID()))
+			{
+				// Remove
+				return true;
+			}
+
+			// Target path should always have at least one segment (copy bind directly on a target struct/object). 
+			if (Binding.GetTargetPath().IsPathEmpty())
+			{
+				return true;
+			}
+
+			// Remove binding if path containing instanced indirections (e.g. instance struct or object) cannot be resolved.
+			// TODO: Try to use core redirect to find new name.
+			{
+				const FEzAbilityDataView* SourceValue = ValidStructs.Find(Binding.GetSourcePath().GetStructID());
+				if (SourceValue && SourceValue->IsValid())
+				{
+					FString Error;
+					TArray<FEzAbilityPropertyPathIndirection> Indirections;
+					if (!Binding.GetSourcePath().ResolveIndirectionsWithValue(*SourceValue, Indirections, &Error))
+					{
+						UE_LOG(LogEzAbility, Verbose, TEXT("Removing binding to %s because binding source path cannot be resolved: %s"),
+							*Binding.GetSourcePath().ToString(), *Error); // Error contains the target path.
+
+						// Remove
+						return true;
+					}
+				}
+			}
+			
+			{
+				const FEzAbilityDataView TargetValue = ValidStructs.FindChecked(Binding.GetTargetPath().GetStructID());
+				FString Error;
+				TArray<FEzAbilityPropertyPathIndirection> Indirections;
+				if (!Binding.GetTargetPath().ResolveIndirectionsWithValue(TargetValue, Indirections, &Error))
+				{
+					UE_LOG(LogEzAbility, Verbose, TEXT("Removing binding to %s because binding target path cannot be resolved: %s"),
+						*Binding.GetSourcePath().ToString(), *Error); // Error contains the target path.
+
+					// Remove
+					return true;
+				}
+			}
+
+			return false;
+		});
+}
+
+bool FEzAbilityEditorPropertyBindings::ContainsAnyStruct(const TSet<const UStruct*>& Structs)
+{
+	auto PathContainsStruct = [&Structs](const FEzAbilityPropertyPath& PropertyPath)
+	{
+		for (const FEzAbilityPropertyPathSegment& Segment : PropertyPath.GetSegments())
+		{
+			if (Structs.Contains(Segment.GetInstanceStruct()))
+			{
+				return true;
+			}
+		}
+		return false;
+	};
+	
+	for (FEzAbilityPropertyPathBinding& PropertyPathBinding : PropertyBindings)
+	{
+		if (PathContainsStruct(PropertyPathBinding.GetSourcePath()))
+		{
+			return true;
+		}
+		if (PathContainsStruct(PropertyPathBinding.GetTargetPath()))
+		{
+			return true;
+		}
+	}
+	
+	return false;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+FEzAbilityBindingLookup::FEzAbilityBindingLookup(IEzAbilityEditorPropertyBindingsOwner* InBindingOwner)
+	: BindingOwner(InBindingOwner)
+{
+}
+
+const FEzAbilityPropertyPath* FEzAbilityBindingLookup::GetPropertyBindingSource(const FEzAbilityPropertyPath& InTargetPath) const
+{
+	check(BindingOwner);
+	const FEzAbilityEditorPropertyBindings* EditorBindings = BindingOwner->GetPropertyEditorBindings();
+	check(EditorBindings);
+
+	return EditorBindings->GetPropertyBindingSource(InTargetPath);
+}
+
+FText FEzAbilityBindingLookup::GetPropertyPathDisplayName(const FEzAbilityPropertyPath& InPath) const
+{
+	check(BindingOwner);
+	const FEzAbilityEditorPropertyBindings* EditorBindings = BindingOwner->GetPropertyEditorBindings();
+	check(EditorBindings);
+
+	FString Result;
+
+	FEzAbilityBindableStructDesc Struct;
+	if (BindingOwner->GetStructByID(InPath.GetStructID(), Struct))
+	{
+		Result = Struct.Name.ToString();
+	}
+
+	Result += TEXT(".") + InPath.ToString();
+
+	return FText::FromString(Result);
+}
+
+const FProperty* FEzAbilityBindingLookup::GetPropertyPathLeafProperty(const FEzAbilityPropertyPath& InPath) const
+{
+	check(BindingOwner);
+	const FEzAbilityEditorPropertyBindings* EditorBindings = BindingOwner->GetPropertyEditorBindings();
+	check(EditorBindings);
+
+	const FProperty* Result = nullptr;
+	FEzAbilityBindableStructDesc Struct;
+	if (BindingOwner->GetStructByID(InPath.GetStructID(), Struct))
+	{
+		TArray<FEzAbilityPropertyPathIndirection> Indirection;
+		if (InPath.ResolveIndirections(Struct.Struct, Indirection) && Indirection.Num() > 0)
+		{
+			return Indirection.Last().GetProperty();
+		}
+	}
+
+	return Result;
+}

+ 58 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityEditorTypes.cpp

@@ -0,0 +1,58 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "EzAbilityEditorTypes.h"
+
+bool FEzAbilityEditorColor::ExportTextItem(FString& OutValueString, const FEzAbilityEditorColor& DefaultValue, UObject* OwnerObject, int32 PortFlags, UObject* ExportRootScope) const
+{
+#if WITH_EDITORONLY_DATA
+	// Only go through this Export path if Copy or Duplicate 
+	if ((PortFlags & (PPF_Copy | PPF_Duplicate)) == 0)
+	{
+		return false;
+	}
+
+	uint32 Count = 0;
+
+	for (FProperty* Property : TFieldRange<FProperty>(FEzAbilityEditorColor::StaticStruct()))
+	{
+		if (!Property->ShouldPort(PortFlags) || Property->HasMetaData(TEXT("StructExportTransient")))
+		{
+			continue;
+		}
+
+		for (int32 Index = 0; Index < Property->ArrayDim; Index++)
+		{
+			FString InnerValue;
+			if (Property->ExportText_InContainer(Index, InnerValue, this, &DefaultValue, OwnerObject, PPF_Delimited | PortFlags, ExportRootScope))
+			{
+				OutValueString += Count++ == 0 ? TEXT('(') : TEXT(',');
+
+				if (Property->ArrayDim == 1)
+				{
+					OutValueString += FString::Printf(TEXT("%s="), *Property->GetName());
+				}
+				else
+				{
+					OutValueString += FString::Printf(TEXT("%s[%i]="), *Property->GetName(), Index);
+				}
+
+				OutValueString += InnerValue;
+			}
+		}
+	}
+
+	if (Count > 0)
+	{
+		OutValueString += TEXT(")");
+	}
+	else
+	{
+		OutValueString += TEXT("()");
+	}
+	return true;
+#else
+	return false;
+#endif
+}
+

+ 258 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityPropertyHelpers.cpp

@@ -0,0 +1,258 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "EzAbilityPropertyHelpers.h"
+#include "EzAbilityEditorNode.h"
+#include "Hash/Blake3.h"
+#include "Misc/EnumerateRange.h"
+#include "Misc/StringBuilder.h"
+#include "UObject/Field.h"
+
+namespace UE::EzAbility::PropertyHelpers
+{
+
+void DispatchPostEditToNodes(UObject& Owner, FPropertyChangedChainEvent& InPropertyChangedEvent)
+{
+	// Walk back from the changed property and look for first FEzAbilityEditorNode, and call the node specific post edit methods.
+	
+	const TDoubleLinkedList<FProperty*>::TDoubleLinkedListNode* CurrentPropNode = InPropertyChangedEvent.PropertyChain.GetHead();
+	const FProperty* HeadProperty = CurrentPropNode->GetValue();
+	check(HeadProperty);
+	if (HeadProperty->GetOwnerClass() != Owner.GetClass())
+	{
+		return;
+	}
+	
+	uint8* CurrentAddress = reinterpret_cast<uint8*>(&Owner);
+	while (CurrentPropNode)
+	{
+		const FProperty* CurrentProperty = CurrentPropNode->GetValue();
+		check(CurrentProperty);
+		CurrentAddress = CurrentAddress + CurrentProperty->GetOffset_ForInternal();
+
+		while (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(CurrentProperty))
+		{
+			FScriptArrayHelper Helper(ArrayProperty, CurrentAddress);
+			const int32 Index = InPropertyChangedEvent.GetArrayIndex(ArrayProperty->GetName());
+			if (!Helper.IsValidIndex(Index))
+			{
+				return;
+			}
+
+			CurrentAddress = Helper.GetRawPtr(Index);
+			CurrentProperty = ArrayProperty->Inner;
+		}
+
+		if (const FStructProperty* StructProperty = CastField<FStructProperty>(CurrentProperty))
+		{
+			if (StructProperty->Struct == FInstancedStruct::StaticStruct())
+			{
+				FInstancedStruct& InstancedStruct = *reinterpret_cast<FInstancedStruct*>(CurrentAddress);
+				CurrentAddress = InstancedStruct.GetMutableMemory();
+			}
+			else if (StructProperty->Struct == FEzAbilityEditorNode::StaticStruct())
+			{
+				FEzAbilityEditorNode& EditorNode = *reinterpret_cast<FEzAbilityEditorNode*>(CurrentAddress);
+				if (FEzAbilityNodeBase* EzAbilityNode = EditorNode.Node.GetMutablePtr<FEzAbilityNodeBase>())
+				{
+					// Check that the path contains EditorNode's: Node, Instance or Instance Object
+					if (const TDoubleLinkedList<FProperty*>::TDoubleLinkedListNode* EditorNodeMemberPropNode = CurrentPropNode->GetNextNode())
+					{
+						// Check that we have a changed property on one of the above properties.
+						if (const TDoubleLinkedList<FProperty*>::TDoubleLinkedListNode* ActiveMemberPropNode = EditorNodeMemberPropNode->GetNextNode()) 
+						{
+							// Update the event
+							const FProperty* EditorNodeChildMember = EditorNodeMemberPropNode->GetValue();
+							check(EditorNodeChildMember);
+
+							// Take copy of the event, we'll modify it.
+							FEditPropertyChain PropertyChainCopy;
+							for (const TDoubleLinkedList<FProperty*>::TDoubleLinkedListNode* Node = InPropertyChangedEvent.PropertyChain.GetHead(); Node->GetNextNode(); Node = Node->GetNextNode())
+							{
+								PropertyChainCopy.AddTail(Node->GetValue());
+							}
+							FPropertyChangedChainEvent PropertyChangedEvent(PropertyChainCopy, InPropertyChangedEvent);
+
+							PropertyChangedEvent.SetActiveMemberProperty(ActiveMemberPropNode->GetValue());
+							PropertyChangedEvent.PropertyChain.SetActiveMemberPropertyNode(PropertyChangedEvent.MemberProperty);
+
+							// To be consistent with the other property chain callbacks, do not cross object boundary.
+							const TDoubleLinkedList<FProperty*>::TDoubleLinkedListNode* ActivePropNode = ActiveMemberPropNode;
+							while (ActivePropNode->GetNextNode())
+							{
+								if (CastField<FObjectProperty>(ActivePropNode->GetValue()))
+								{
+									break;
+								}
+								ActivePropNode = ActivePropNode->GetNextNode();
+							}
+							
+							PropertyChangedEvent.Property = ActivePropNode->GetValue();
+							PropertyChangedEvent.PropertyChain.SetActivePropertyNode(PropertyChangedEvent.Property);
+
+							if (EditorNodeChildMember->GetFName() == GET_MEMBER_NAME_CHECKED(FEzAbilityEditorNode, Node))
+							{
+								EzAbilityNode->PostEditNodeChangeChainProperty(PropertyChangedEvent, EditorNode.GetInstance());
+							}
+							else if (EditorNodeChildMember->GetFName() == GET_MEMBER_NAME_CHECKED(FEzAbilityEditorNode, Instance))
+							{
+								if (EditorNode.Instance.IsValid())
+								{
+									EzAbilityNode->PostEditInstanceDataChangeChainProperty(PropertyChangedEvent, FEzAbilityDataView(EditorNode.Instance));
+								}
+							}
+							else if (EditorNodeChildMember->GetFName() == GET_MEMBER_NAME_CHECKED(FEzAbilityEditorNode, InstanceObject))
+							{
+								if (EditorNode.InstanceObject)
+								{
+									EzAbilityNode->PostEditInstanceDataChangeChainProperty(PropertyChangedEvent, FEzAbilityDataView(EditorNode.InstanceObject));
+								}
+							}
+						}
+					}
+				}
+
+				break;
+			}
+
+			CurrentPropNode = CurrentPropNode->GetNextNode();
+		}
+		else
+		{
+			break;
+		}
+	}
+}
+
+
+FGuid MakeDeterministicID(const UObject& Owner, const FString& PropertyPath, const uint64 Seed)
+{
+	// From FGuid::NewDeterministicGuid(FStringView ObjectPath, uint64 Seed)
+	
+	// Convert the objectpath to utf8 so that whether TCHAR is UTF8 or UTF16 does not alter the hash.
+	TUtf8StringBuilder<1024> Utf8ObjectPath(InPlace, Owner.GetPathName());
+	TUtf8StringBuilder<1024> Utf8PropertyPath(InPlace, PropertyPath);
+
+	FBlake3 Builder;
+
+	// Hash this as the namespace of the Version 3 UUID, to avoid collisions with any other guids created using Blake3.
+	static FGuid BaseVersion(TEXT("bf324a38-a445-45a4-8921-249554b58189"));
+	Builder.Update(&BaseVersion, sizeof(FGuid));
+	Builder.Update(Utf8ObjectPath.GetData(), Utf8ObjectPath.Len() * sizeof(UTF8CHAR));
+	Builder.Update(Utf8PropertyPath.GetData(), Utf8PropertyPath.Len() * sizeof(UTF8CHAR));
+	Builder.Update(&Seed, sizeof(Seed));
+
+	const FBlake3Hash Hash = Builder.Finalize();
+
+	return FGuid::NewGuidFromHash(Hash);
+}
+
+bool HasOptionalMetadata(const FProperty& Property)
+{
+	return Property.HasMetaData(TEXT("Optional"));
+}
+
+}; // UE::EzAbility::PropertyHelpers
+
+// ------------------------------------------------------------------------------
+// FEzAbilityEditPropertyPath
+// ------------------------------------------------------------------------------
+FEzAbilityEditPropertyPath::FEzAbilityEditPropertyPath(const UStruct* BaseStruct, const FString& InPath)
+{
+	TArray<FString> PathSegments;
+	InPath.ParseIntoArray(PathSegments, TEXT("."));
+
+	const UStruct* CurrBase = BaseStruct;
+	for (const FString& Segment : PathSegments)
+	{
+		const FName PropertyName(Segment);
+		if (const FProperty* Property = CurrBase->FindPropertyByName(PropertyName))
+		{
+			Path.Emplace(Property, PropertyName);
+
+			if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property))
+			{
+				Property = ArrayProperty->Inner;
+			}
+
+			if (const FStructProperty* StructProperty = CastField<FStructProperty>(Property))
+			{
+				CurrBase = StructProperty->Struct;
+			}
+			else if (const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(Property))
+			{
+				CurrBase = ObjectProperty->PropertyClass;
+			}
+		}
+		else
+		{
+			checkf(false, TEXT("Path %s id not part of type %s."), *InPath, *GetNameSafe(BaseStruct));
+			Path.Reset();
+			break;
+		}
+	}
+}
+
+FEzAbilityEditPropertyPath::FEzAbilityEditPropertyPath(const FPropertyChangedChainEvent& PropertyChangedEvent)
+{
+	FEditPropertyChain::TDoubleLinkedListNode* PropertyNode = PropertyChangedEvent.PropertyChain.GetActiveMemberNode();
+	while (PropertyNode != nullptr)
+	{
+		if (FProperty* Property = PropertyNode->GetValue())
+		{
+			const FName PropertyName = Property->GetFName(); 
+			const int32 ArrayIndex = PropertyChangedEvent.GetArrayIndex(PropertyName.ToString());
+			Path.Emplace(Property, PropertyName, ArrayIndex);
+		}
+		PropertyNode = PropertyNode->GetNextNode();
+	}
+}
+
+FEzAbilityEditPropertyPath::FEzAbilityEditPropertyPath(const FEditPropertyChain& PropertyChain)
+{
+	FEditPropertyChain::TDoubleLinkedListNode* PropertyNode = PropertyChain.GetActiveMemberNode();
+	while (PropertyNode != nullptr)
+	{
+		if (FProperty* Property = PropertyNode->GetValue())
+		{
+			const FName PropertyName = Property->GetFName(); 
+			Path.Emplace(Property, PropertyName, INDEX_NONE);
+		}
+		PropertyNode = PropertyNode->GetNextNode();
+	}
+}
+
+bool FEzAbilityEditPropertyPath::ContainsPath(const FEzAbilityEditPropertyPath& InPath) const
+{
+	if (InPath.Path.Num() > Path.Num())
+    {
+    	return false;
+    }
+
+    for (TConstEnumerateRef<FEzAbilityEditPropertySegment> Segment : EnumerateRange(InPath.Path))
+    {
+    	if (Segment->PropertyName != Path[Segment.GetIndex()].PropertyName)
+    	{
+    		return false;
+    	}
+    }
+    return true;
+}
+
+/** @return true if the property path is exactly the specified path. */
+bool FEzAbilityEditPropertyPath::IsPathExact(const FEzAbilityEditPropertyPath& InPath) const
+{
+	if (InPath.Path.Num() != Path.Num())
+	{
+		return false;
+	}
+
+	for (TConstEnumerateRef<FEzAbilityEditPropertySegment> Segment : EnumerateRange(InPath.Path))
+	{
+		if (Segment->PropertyName != Path[Segment.GetIndex()].PropertyName)
+		{
+			return false;
+		}
+	}
+	return true;
+}

+ 522 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Private/EzAbilityState.cpp

@@ -0,0 +1,522 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+
+#include "EzAbilityState.h"
+
+#include "EzAbilityDelegates.h"
+#include "EzAbilityEditorData.h"
+#include "EzAbilityPropertyHelpers.h"
+#include "Condition/EzAbilityCondition.h"
+#include "Task/EzAbilityTask.h"
+
+//////////////////////////////////////////////////////////////////////////
+// FEzAbilityStateParameters
+
+void FEzAbilityStateParameters::RemoveUnusedOverrides()
+{
+	// Remove overrides that do not exists anymore
+	if (!PropertyOverrides.IsEmpty())
+	{
+		if (const UPropertyBag* Bag = Parameters.GetPropertyBagStruct())
+		{
+			for (TArray<FGuid>::TIterator It = PropertyOverrides.CreateIterator(); It; ++It)
+			{
+				if (!Bag->FindPropertyDescByID(*It))
+				{
+					It.RemoveCurrentSwap();
+				}
+			}
+		}
+	}
+}
+
+//////////////////////////////////////////////////////////////////////////
+// FEzAbilityTransition
+
+FEzAbilityTransition::FEzAbilityTransition(const EEzAbilityTransitionTrigger InTrigger, const EEzAbilityTransitionType InType, const UEzAbilityState* InState)
+	: Trigger(InTrigger)
+{
+	State = InState ? InState->GetLinkToState() : FEzAbilityStateLink(InType);
+}
+
+FEzAbilityTransition::FEzAbilityTransition(const EEzAbilityTransitionTrigger InTrigger, const FGameplayTag InEventTag, const EEzAbilityTransitionType InType, const UEzAbilityState* InState)
+	: Trigger(InTrigger)
+	, EventTag(InEventTag)
+{
+	State = InState ? InState->GetLinkToState() : FEzAbilityStateLink(InType);
+}
+
+//////////////////////////////////////////////////////////////////////////
+// UEzAbilityState
+
+UEzAbilityState::UEzAbilityState(const FObjectInitializer& ObjectInitializer)
+	: Super(ObjectInitializer)
+	, ID(FGuid::NewGuid())
+{
+	Parameters.ID = FGuid::NewGuid();
+}
+
+UEzAbilityState::~UEzAbilityState()
+{
+	UE::EzAbility::Delegates::OnPostCompile.RemoveAll(this);
+}
+
+void UEzAbilityState::PostInitProperties()
+{
+	Super::PostInitProperties();
+	
+	UE::EzAbility::Delegates::OnPostCompile.AddUObject(this, &UEzAbilityState::OnTreeCompiled);
+}
+
+void UEzAbilityState::OnTreeCompiled(const UEzAbility& EzAbility)
+{
+	if (&EzAbility == LinkedAsset)
+	{
+		UpdateParametersFromLinkedSubtree();
+	}
+}
+
+void UEzAbilityState::PreEditChange(FEditPropertyChain& PropertyAboutToChange)
+{
+	Super::PreEditChange(PropertyAboutToChange);
+
+	const FEzAbilityEditPropertyPath PropertyChainPath(PropertyAboutToChange);
+
+	static const FEzAbilityEditPropertyPath StateTypePath(UEzAbilityState::StaticClass(), TEXT("Type"));
+
+	if (PropertyChainPath.IsPathExact(StateTypePath))
+	{
+		// If transitioning from linked state, reset the parameters
+		if (Type == EEzAbilityStateType::Linked
+			|| Type == EEzAbilityStateType::LinkedAsset)
+		{
+			Parameters.Reset();
+		}
+	}
+}
+
+void UEzAbilityState::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent)
+{
+	Super::PostEditChangeChainProperty(PropertyChangedEvent);
+
+	const FEzAbilityEditPropertyPath ChangePropertyPath(PropertyChangedEvent);
+	
+	auto CopyBindings = [this](const FGuid FromStructID, const FGuid ToStructID)
+	{
+		if (UEzAbilityEditorData* EditorData = GetTypedOuter<UEzAbilityEditorData>())
+		{
+		    if (FromStructID.IsValid())
+            {
+                if (FEzAbilityEditorPropertyBindings* Bindings = EditorData->GetPropertyEditorBindings())
+                {
+                	Bindings->CopyBindings(FromStructID, ToStructID);
+                }
+            }
+		}
+	};
+
+	
+	static const FEzAbilityEditPropertyPath StateNamePath(UEzAbilityState::StaticClass(), TEXT("Name"));
+	static const FEzAbilityEditPropertyPath StateTypePath(UEzAbilityState::StaticClass(), TEXT("Type"));
+	static const FEzAbilityEditPropertyPath SelectionBehaviorPath(UEzAbilityState::StaticClass(), TEXT("SelectionBehavior"));
+	static const FEzAbilityEditPropertyPath StateLinkedSubtreePath(UEzAbilityState::StaticClass(), TEXT("LinkedSubtree"));
+	static const FEzAbilityEditPropertyPath StateLinkedAssetPath(UEzAbilityState::StaticClass(), TEXT("LinkedAsset"));
+	static const FEzAbilityEditPropertyPath StateParametersPath(UEzAbilityState::StaticClass(), TEXT("Parameters"));
+	static const FEzAbilityEditPropertyPath StateTasksPath(UEzAbilityState::StaticClass(), TEXT("Tasks"));
+	static const FEzAbilityEditPropertyPath StateEnterConditionsPath(UEzAbilityState::StaticClass(), TEXT("EnterConditions"));
+	static const FEzAbilityEditPropertyPath StateTransitionsPath(UEzAbilityState::StaticClass(), TEXT("Transitions"));
+	static const FEzAbilityEditPropertyPath StateTransitionsConditionsPath(UEzAbilityState::StaticClass(), TEXT("Transitions.Conditions"));
+	static const FEzAbilityEditPropertyPath StateTransitionsIDPath(UEzAbilityState::StaticClass(), TEXT("Transitions.ID"));
+
+
+	// Broadcast name changes so that the UI can update.
+	if (ChangePropertyPath.IsPathExact(StateNamePath))
+	{
+		const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+		if (ensure(EzAbility))
+		{
+			UE::EzAbility::Delegates::OnIdentifierChanged.Broadcast(*EzAbility);
+		}
+	}
+
+	// Broadcast selection type changes so that the UI can update.
+	if (ChangePropertyPath.IsPathExact(SelectionBehaviorPath))
+	{
+		const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+		if (ensure(EzAbility))
+		{
+			UE::EzAbility::Delegates::OnIdentifierChanged.Broadcast(*EzAbility);
+		}
+	}
+	
+	if (ChangePropertyPath.IsPathExact(StateTypePath))
+	{
+		// Remove any tasks and evaluators when they are not used.
+		if (Type == EEzAbilityStateType::Group || Type == EEzAbilityStateType::Linked || Type == EEzAbilityStateType::LinkedAsset)
+		{
+			Tasks.Reset();
+		}
+
+		// If transitioning from linked state, reset the linked state.
+		if (Type != EEzAbilityStateType::Linked)
+		{
+			LinkedSubtree = FEzAbilityStateLink();
+		}
+		if (Type != EEzAbilityStateType::LinkedAsset)
+		{
+			LinkedAsset = nullptr;
+		}
+
+		if (Type == EEzAbilityStateType::Linked
+			|| Type == EEzAbilityStateType::LinkedAsset)
+		{
+			// Linked parameter layout is fixed, and copied from the linked target state.
+			Parameters.bFixedLayout = true;
+			UpdateParametersFromLinkedSubtree();
+			SelectionBehavior = EEzAbilityStateSelectionBehavior::TrySelectChildrenInOrder;
+		}
+		else
+		{
+			// Other layouts can be edited
+			Parameters.bFixedLayout = false;
+		}
+	}
+
+	// When switching to new state, update the parameters.
+	if (ChangePropertyPath.IsPathExact(StateLinkedSubtreePath))
+	{
+		if (Type == EEzAbilityStateType::Linked)
+		{
+			UpdateParametersFromLinkedSubtree();
+		}
+	}
+	
+	if (ChangePropertyPath.IsPathExact(StateLinkedAssetPath))
+	{
+		if (Type == EEzAbilityStateType::LinkedAsset)
+		{
+			UpdateParametersFromLinkedSubtree();
+		}
+	}
+
+	// Broadcast subtree parameter layout edits so that the linked states can adapt.
+	if (ChangePropertyPath.IsPathExact(StateParametersPath))
+	{
+		if (!(Type == EEzAbilityStateType::Linked
+				|| Type == EEzAbilityStateType::LinkedAsset))
+		{
+			const UEzAbility* EzAbility = GetTypedOuter<UEzAbility>();
+			if (ensure(EzAbility))
+			{
+				UE::EzAbility::Delegates::OnStateParametersChanged.Broadcast(*EzAbility, ID);
+			}
+		}
+	}
+
+	// Reset delay on completion transitions
+	if (ChangePropertyPath.ContainsPath(StateTransitionsPath))
+	{
+		const int32 TransitionsIndex = ChangePropertyPath.GetPropertyArrayIndex(StateTransitionsPath);
+		if (Transitions.IsValidIndex(TransitionsIndex))
+		{
+			FEzAbilityTransition& Transition = Transitions[TransitionsIndex];
+
+			if (EnumHasAnyFlags(Transition.Trigger, EEzAbilityTransitionTrigger::OnStateCompleted))
+			{
+				Transition.bDelayTransition = false;
+			}
+		}
+	}
+
+	// Ensure unique ID on duplicated items.
+	if (PropertyChangedEvent.ChangeType == EPropertyChangeType::Duplicate)
+	{
+		
+		// Tasks
+		if (ChangePropertyPath.IsPathExact(StateTasksPath))
+		{
+			const int32 ArrayIndex = ChangePropertyPath.GetPropertyArrayIndex(StateTasksPath);
+			if (Tasks.IsValidIndex(ArrayIndex))
+			{
+ 				if (FEzAbilityTask* Task = Tasks[ArrayIndex].Node.GetMutablePtr<FEzAbilityTask>())
+				{
+					Task->Name = FName(Task->Name.ToString() + TEXT(" Duplicate"));
+				}
+				const FGuid OldStructID = Tasks[ArrayIndex].ID; 
+				Tasks[ArrayIndex].ID = FGuid::NewGuid();
+				CopyBindings(OldStructID, Tasks[ArrayIndex].ID);
+			}
+		}
+
+		// Enter conditions
+		if (ChangePropertyPath.IsPathExact(StateEnterConditionsPath))
+		{
+			const int32 ArrayIndex = ChangePropertyPath.GetPropertyArrayIndex(StateEnterConditionsPath);
+			if (EnterConditions.IsValidIndex(ArrayIndex))
+			{
+				if (FEzAbilityCondition* Condition = EnterConditions[ArrayIndex].Node.GetMutablePtr<FEzAbilityCondition>())
+				{
+					Condition->Name = FName(Condition->Name.ToString() + TEXT(" Duplicate"));
+				}
+				const FGuid OldStructID = EnterConditions[ArrayIndex].ID; 
+				EnterConditions[ArrayIndex].ID = FGuid::NewGuid();
+				CopyBindings(OldStructID, EnterConditions[ArrayIndex].ID);
+			}
+		}
+
+		// Transitions
+		if (ChangePropertyPath.IsPathExact(StateTransitionsPath))
+		{
+			const int32 TransitionsIndex = ChangePropertyPath.GetPropertyArrayIndex(StateTransitionsPath);
+			if (Transitions.IsValidIndex(TransitionsIndex))
+			{
+				FEzAbilityTransition& Transition = Transitions[TransitionsIndex];
+				Transition.ID = FGuid::NewGuid();
+
+				// Update conditions
+				for (FEzAbilityEditorNode& Condition : Transition.Conditions)
+				{
+					const FGuid OldStructID = Condition.ID;
+					Condition.ID = FGuid::NewGuid();
+					CopyBindings(OldStructID, Condition.ID);
+				}
+			}
+		}
+
+		// Transition conditions
+		if (ChangePropertyPath.IsPathExact(StateTransitionsConditionsPath))
+		{
+			const int32 TransitionsIndex = ChangePropertyPath.GetPropertyArrayIndex(StateTransitionsPath);
+			const int32 ConditionsIndex = ChangePropertyPath.GetPropertyArrayIndex(StateTransitionsConditionsPath);
+
+			if (Transitions.IsValidIndex(TransitionsIndex))
+			{
+				FEzAbilityTransition& Transition = Transitions[TransitionsIndex];
+				if (Transition.Conditions.IsValidIndex(ConditionsIndex))
+				{
+					if (FEzAbilityCondition* Condition = Transition.Conditions[ConditionsIndex].Node.GetMutablePtr<FEzAbilityCondition>())
+					{
+						Condition->Name = FName(Condition->Name.ToString() + TEXT(" Duplicate"));
+					}
+					const FGuid OldStructID = Transition.Conditions[ConditionsIndex].ID;
+					Transition.Conditions[ConditionsIndex].ID = FGuid::NewGuid();
+					CopyBindings(OldStructID, Transition.Conditions[ConditionsIndex].ID);
+				}
+			}
+		}
+	}
+	
+	// Set default state to root and Id on new transitions.
+	if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ArrayAdd)
+	{
+		if (ChangePropertyPath.IsPathExact(StateTransitionsPath))
+		{
+			const int32 TransitionsIndex = ChangePropertyPath.GetPropertyArrayIndex(StateTransitionsPath);
+			if (Transitions.IsValidIndex(TransitionsIndex))
+			{
+				FEzAbilityTransition& Transition = Transitions[TransitionsIndex];
+				Transition.Trigger = EEzAbilityTransitionTrigger::OnStateCompleted;
+				const UEzAbilityState* RootState = GetRootState();
+				Transition.State = RootState->GetLinkToState();
+				Transition.ID = FGuid::NewGuid();
+			}
+		}
+	}
+
+	// Remove bindings when bindable nodes are removed.
+	if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ArrayRemove)
+	{
+		if (ChangePropertyPath.IsPathExact(StateTasksPath)
+			|| ChangePropertyPath.IsPathExact(StateEnterConditionsPath)
+			|| ChangePropertyPath.IsPathExact(StateTransitionsConditionsPath))
+		{
+			if (UEzAbilityEditorData* TreeData = GetTypedOuter<UEzAbilityEditorData>())
+			{
+				TreeData->Modify();
+				FEzAbilityEditorPropertyBindings* Bindings = TreeData->GetPropertyEditorBindings();
+				check(Bindings);
+				TMap<FGuid, const FEzAbilityDataView> AllStructValues;
+				TreeData->GetAllStructValues(AllStructValues);
+				Bindings->RemoveUnusedBindings(AllStructValues);
+			}
+		}
+	}
+
+	UE::EzAbility::PropertyHelpers::DispatchPostEditToNodes(*this, PropertyChangedEvent);
+}
+
+void UEzAbilityState::PostLoad()
+{
+	Super::PostLoad();
+
+	// Make sure state has transactional flags to make it work with undo (to fix a bug where root states were created without this flag).
+	if (!HasAnyFlags(RF_Transactional))
+	{
+		SetFlags(RF_Transactional);
+	}
+
+#if WITH_EDITORONLY_DATA
+	// const int32 CurrentVersion = GetLinkerCustomVersion(FEzAbilityCustomVersion::GUID);
+	// if (CurrentVersion < FEzAbilityCustomVersion::AddedTransitionIds)
+	// {
+	// 	// Make guids for transitions. These need to be deterministic when upgrading because of cooking.
+	// 	for (int32 Index = 0; Index < Transitions.Num(); Index++)
+	// 	{
+	// 		FEzAbilityTransition& Transition = Transitions[Index];
+	// 		Transition.ID = FGuid::NewDeterministicGuid(GetPathName(), Index);
+	// 	}
+	// }
+	//
+	// if (CurrentVersion < FEzAbilityCustomVersion::OverridableStateParameters)
+	// {
+	// 	// In earlier versions, all parameters were overwritten.
+	// 	if (const UPropertyBag* Bag = Parameters.Parameters.GetPropertyBagStruct())
+	// 	{
+	// 		for (const FPropertyBagPropertyDesc& Desc : Bag->GetPropertyDescs())
+	// 		{
+	// 			Parameters.PropertyOverrides.Add(Desc.ID);
+	// 		}
+	// 	}
+	// }
+	
+#endif // WITH_EDITORONLY_DATA
+
+}
+
+void UEzAbilityState::UpdateParametersFromLinkedSubtree()
+{
+	if (const FInstancedPropertyBag* DefaultParameters = GetDefaultParameters())
+	{
+		Parameters.Parameters.MigrateToNewBagInstanceWithOverrides(*DefaultParameters, Parameters.PropertyOverrides);
+		Parameters.RemoveUnusedOverrides();
+	}
+	else
+	{
+		Parameters.Reset();
+	}
+}
+
+void UEzAbilityState::SetParametersPropertyOverridden(const FGuid PropertyID, const bool bIsOverridden)
+{
+	if (bIsOverridden)
+	{
+		Parameters.PropertyOverrides.AddUnique(PropertyID);
+	}
+	else
+	{
+		Parameters.PropertyOverrides.Remove(PropertyID);
+		UpdateParametersFromLinkedSubtree();
+
+		// Remove binding when override is removed.
+		if (UEzAbilityEditorData* EditorData = GetTypedOuter<UEzAbilityEditorData>())
+		{
+			if (FEzAbilityEditorPropertyBindings* Bindings = EditorData->GetPropertyEditorBindings())
+			{
+				if (const UPropertyBag* ParametersBag = Parameters.Parameters.GetPropertyBagStruct())
+				{
+					if (const FPropertyBagPropertyDesc* Desc = ParametersBag->FindPropertyDescByID(PropertyID))
+					{
+						check(Desc->CachedProperty);
+
+						EditorData->Modify();
+
+						FEzAbilityPropertyPath Path(Parameters.ID, Desc->CachedProperty->GetFName());
+						Bindings->RemovePropertyBindings(Path);
+					}
+				}
+			}
+		}
+	}
+}
+
+const FInstancedPropertyBag* UEzAbilityState::GetDefaultParameters() const
+{
+	if (Type == EEzAbilityStateType::Linked)
+	{
+		if (const UEzAbilityEditorData* TreeData = GetTypedOuter<UEzAbilityEditorData>())
+		{
+			if (const UEzAbilityState* LinkTargetState = TreeData->GetStateByID(LinkedSubtree.ID))
+			{
+				return &LinkTargetState->Parameters.Parameters;
+			}
+		}
+	}
+	else if (Type == EEzAbilityStateType::LinkedAsset)
+	{
+		if (LinkedAsset)
+		{
+			return &LinkedAsset->GetDefaultParameters();
+		}
+	}
+
+	return nullptr;
+}
+
+const UEzAbilityState* UEzAbilityState::GetRootState() const
+{
+	const UEzAbilityState* RootState = this;
+	while (RootState->Parent != nullptr)
+	{
+		RootState = RootState->Parent;
+	}
+	return RootState;
+}
+
+const UEzAbilityState* UEzAbilityState::GetNextSiblingState() const
+{
+	if (!Parent)
+	{
+		return nullptr;
+	}
+	for (int32 ChildIdx = 0; ChildIdx < Parent->Children.Num(); ChildIdx++)
+	{
+		if (Parent->Children[ChildIdx] == this)
+		{
+			const int NextIdx = ChildIdx + 1;
+
+			// Select the next enabled sibling
+			if (NextIdx < Parent->Children.Num() && Parent->Children[NextIdx]->bEnabled)
+			{
+				return Parent->Children[NextIdx];
+			}
+			break;
+		}
+	}
+	return nullptr;
+}
+
+const UEzAbilityState* UEzAbilityState::GetNextSelectableSiblingState() const
+{
+	if (!Parent)
+	{
+		return nullptr;
+	}
+
+	const int32 StartChildIndex = Parent->Children.IndexOfByKey(this);
+	if (StartChildIndex == INDEX_NONE)
+	{
+		return nullptr;
+	}
+	
+	for (int32 ChildIdx = StartChildIndex + 1; ChildIdx < Parent->Children.Num(); ChildIdx++)
+	{
+		// Select the next enabled and selectable sibling
+		const UEzAbilityState* State =Parent->Children[ChildIdx];
+		if (State->SelectionBehavior != EEzAbilityStateSelectionBehavior::None
+			&& State->bEnabled)
+		{
+			return State;
+		}
+	}
+	
+	return nullptr;
+}
+
+FEzAbilityStateLink UEzAbilityState::GetLinkToState() const
+{
+	FEzAbilityStateLink Link(EEzAbilityTransitionType::GotoState);
+	Link.Name = Name;
+	Link.ID = ID;
+	return Link;
+}
+

+ 2 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditor.h

@@ -13,3 +13,5 @@ public:
 	virtual void StartupModule() override;
 	virtual void ShutdownModule() override;
 };
+
+EZABILITYEDITOR_API DECLARE_LOG_CATEGORY_EXTERN(LogEzAbilityEditor, Log, All);

+ 284 - 3
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditorData.h

@@ -3,14 +3,295 @@
 #pragma once
 
 #include "CoreMinimal.h"
-#include "UObject/Object.h"
+#include "EzAbilityEditorPropertyBindings.h"
+#include "EzAbilityEditorTypes.h"
+#include "EzAbilityState.h"
+#include "Debugger/EzAbilityDebuggerTypes.h"
 #include "EzAbilityEditorData.generated.h"
 
+struct FEzAbilityBindableStructDesc;
+struct FEzAbilityEditorPropertyPath;
+
+class UEzAbilitySchema;
+
+USTRUCT()
+struct FEzAbilityEditorBreakpoint
+{
+	GENERATED_BODY()
+
+	FEzAbilityEditorBreakpoint() = default;
+	explicit FEzAbilityEditorBreakpoint(const FGuid& ID, const EEzAbilityBreakpointType BreakpointType)
+		: ID(ID)
+		, BreakpointType(BreakpointType)
+	{
+	}
+
+	/** Unique Id of the Node or State associated to the breakpoint. */
+	UPROPERTY()
+	FGuid ID;
+
+	/** The event type that should trigger the breakpoint (e.g. OnEnter, OnExit, etc.). */
+	UPROPERTY()
+	EEzAbilityBreakpointType BreakpointType = EEzAbilityBreakpointType::Unset;
+};
+
+UENUM()
+enum class EEzAbilityVisitor : uint8
+{
+	Continue,
+	Break,
+};
+
 /**
- * 
+ * Edit time data for EzAbility asset. This data gets baked into runtime format before being used by the EzAbilityInstance.
  */
 UCLASS(BlueprintType, EditInlineNew, CollapseCategories)
-class EZABILITYEDITOR_API UEzAbilityEditorData : public UObject
+class EZABILITYEDITOR_API UEzAbilityEditorData : public UObject, public IEzAbilityEditorPropertyBindingsOwner
 {
 	GENERATED_BODY()
+	
+public:
+	UEzAbilityEditorData();
+
+	virtual void PostInitProperties() override;
+	
+	// IEzAbilityEditorPropertyBindingsOwner
+	virtual void GetAccessibleStructs(const FGuid TargetStructID, TArray<FEzAbilityBindableStructDesc>& OutStructDescs) const override;
+	virtual bool GetStructByID(const FGuid StructID, FEzAbilityBindableStructDesc& OutStructDesc) const override;
+	virtual bool GetDataViewByID(const FGuid StructID, FEzAbilityDataView& OutDataView) const override;
+	virtual FEzAbilityEditorPropertyBindings* GetPropertyEditorBindings() override { return &EditorBindings; }
+	// ~IEzAbilityEditorPropertyBindingsOwner
+
+#if WITH_EDITOR
+	using FReplacementObjectMap = TMap<UObject*, UObject*>;
+	void OnObjectsReinstanced(const FReplacementObjectMap& ObjectMap);
+	void OnUserDefinedStructReinstanced(const UUserDefinedStruct& UserDefinedStruct);
+	void OnParametersChanged(const UEzAbility& EzAbility);
+	virtual void BeginDestroy() override;
+	virtual void PostLoad() override;
+	virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override;
+#endif
+
+	/** @returns parent state of a struct, or nullptr if not found. */
+	const UEzAbilityState* GetStateByStructID(const FGuid TargetStructID) const;
+
+	/** @returns state based on its ID, or nullptr if not found. */
+	const UEzAbilityState* GetStateByID(const FGuid StateID) const;
+
+	/** @returns mutable state based on its ID, or nullptr if not found. */
+	UEzAbilityState* GetMutableStateByID(const FGuid StateID);
+
+	/** @returns the IDs and instance values of all bindable structs in the EzAbility. */
+	void GetAllStructValues(TMap<FGuid, const FEzAbilityDataView>& AllValues) const;
+
+	/**
+	* Iterates over all structs that are related to binding
+	* @param InFunc function called at each node, should return true if visiting is continued or false to stop.
+	*/
+	EEzAbilityVisitor VisitHierarchy(TFunctionRef<EEzAbilityVisitor(UEzAbilityState& State, UEzAbilityState* ParentState)> InFunc) const;
+
+	/**
+	 * Iterates over all structs at the global level (context, tree parameters, evaluators, global tasks) that are related to binding.
+	 * @param InFunc function called at each node, should return true if visiting is continued or false to stop.
+	 */
+	EEzAbilityVisitor VisitGlobalNodes(TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)> InFunc) const;
+
+	/**
+	 * Iterates over all structs in the state hierarchy that are related to binding.
+	 * @param InFunc function called at each node, should return true if visiting is continued or false to stop.
+	 */
+	EEzAbilityVisitor VisitHierarchyNodes(TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)> InFunc) const;
+
+	/**
+	 * Iterates over all structs that are related to binding.
+	 * @param InFunc function called at each node, should return true if visiting is continued or false to stop.
+	 */
+	EEzAbilityVisitor VisitAllNodes(TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)> InFunc) const;
+
+	/**
+	 * Iterates over all nodes in a given state.
+	 * @param InFunc function called at each node, should return true if visiting is continued or false to stop.
+	 */
+	EEzAbilityVisitor VisitStateNodes(const UEzAbilityState& State, TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FEzAbilityBindableStructDesc& Desc, const FEzAbilityDataView Value)> InFunc) const;
+
+	/**
+	 * Returns array of nodes along the execution path, up to the TargetStruct.
+	 * @param Path The states to visit during the check
+	 * @param TargetStructID The ID of the node where to stop.
+	 * @param OutStructDescs Array of nodes accessible on the given path.  
+	 */
+	void GetAccessibleStructs(const TConstArrayView<const UEzAbilityState*> Path, const FGuid TargetStructID, TArray<FEzAbilityBindableStructDesc>& OutStructDescs) const;
+
+	/**
+	 * Finds a bindable context struct based on name and type.
+	 * @param ObjectType Object type to match
+	 * @param ObjectNameHint Name to use if multiple context objects of same type are found. 
+	 */
+	FEzAbilityBindableStructDesc FindContextData(const UStruct* ObjectType, const FString ObjectNameHint) const;
+
+	UE_DEPRECATED(5.3, "Use VisitHierarchyNodes with State, Desc, Value instead.")
+	void VisitHierarchyNodes(TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FGuid& ID, const FName& Name, const EEzAbilityNodeType NodeType, const UScriptStruct* NodeStruct, const UStruct* InstanceStruct)> InFunc) const;
+
+	UE_DEPRECATED(5.3, "Use VisitStateNodes with State, Desc, Value instead.")
+	EEzAbilityVisitor VisitStateNodes(const UEzAbilityState& State, TFunctionRef<EEzAbilityVisitor(const UEzAbilityState* State, const FGuid& ID, const FName& Name, const EEzAbilityNodeType NodeType, const UScriptStruct* NodeStruct, const UStruct* InstanceStruct)> InFunc) const;
+
+	UE_DEPRECATED(5.3, "Use GetAllStructValues with values instead.")
+	void GetAllStructIDs(TMap<FGuid, const UStruct*>& AllStructs) const;
+
+	void ReparentStates();
+	
+	// EzAbility Builder API
+
+	/**
+	 * Adds new Subtree with specified name.
+	 * @return Pointer to the new Subtree.
+	 */
+	UEzAbilityState& AddSubTree(const FName Name)
+	{
+		UEzAbilityState* SubTreeState = NewObject<UEzAbilityState>(this, FName(), RF_Transactional);
+		check(SubTreeState);
+		SubTreeState->Name = Name;
+		SubTrees.Add(SubTreeState);
+		return *SubTreeState;
+	}
+
+	/**
+	 * Adds new Subtree named "Root".
+	 * @return Pointer to the new Subtree.
+	 */
+	UEzAbilityState& AddRootState()
+	{
+		return AddSubTree(FName(TEXT("Root")));
+	}
+
+	/**
+	 * Adds Evaluator of specified type.
+	 * @return reference to the new Evaluator. 
+	 */
+	template<typename T, typename... TArgs>
+	TEzAbilityEditorNode<T>& AddEvaluator(TArgs&&... InArgs)
+	{
+		FEzAbilityEditorNode& EditorNode = Evaluators.AddDefaulted_GetRef();
+		EditorNode.ID = FGuid::NewGuid();
+		EditorNode.Node.InitializeAs<T>(Forward<TArgs>(InArgs)...);
+		T& Eval = EditorNode.Node.GetMutable<T>();
+		if (const UScriptStruct* InstanceType = Cast<const UScriptStruct>(Eval.GetInstanceDataType()))
+		{
+			EditorNode.Instance.InitializeAs(InstanceType);
+		}
+		return static_cast<TEzAbilityEditorNode<T>&>(EditorNode);
+	}
+
+	/**
+	 * Adds Global Task of specified type.
+	 * @return reference to the new task. 
+	 */
+	template<typename T, typename... TArgs>
+	TEzAbilityEditorNode<T>& AddGlobalTask(TArgs&&... InArgs)
+	{
+		FEzAbilityEditorNode& EditorNode = GlobalTasks.AddDefaulted_GetRef();
+		EditorNode.ID = FGuid::NewGuid();
+		EditorNode.Node.InitializeAs<T>(Forward<TArgs>(InArgs)...);
+		T& Task = EditorNode.Node.GetMutable<T>();
+		if (const UScriptStruct* InstanceType = Cast<const UScriptStruct>(Task.GetInstanceDataType()))
+		{
+			EditorNode.Instance.InitializeAs(InstanceType);
+		}
+		return static_cast<TEzAbilityEditorNode<T>&>(EditorNode);
+	}
+
+	/**
+	 * Adds property binding between two structs.
+	 */
+	void AddPropertyBinding(const FEzAbilityPropertyPath& SourcePath, const FEzAbilityPropertyPath& TargetPath)
+	{
+		EditorBindings.AddPropertyBinding(SourcePath, TargetPath);
+	}
+
+PRAGMA_DISABLE_DEPRECATION_WARNINGS
+	UE_DEPRECATED(5.3, "Use version with FEzAbilityPropertyPath instead.")
+	void AddPropertyBinding(const FEzAbilityEditorPropertyPath& SourcePath, const FEzAbilityEditorPropertyPath& TargetPath);
+PRAGMA_ENABLE_DEPRECATION_WARNINGS
+
+	/**
+	 * Adds property binding between two structs.
+	 */
+	bool AddPropertyBinding(const FEzAbilityEditorNode& SourceNode, const FString SourcePathStr, const FEzAbilityEditorNode& TargetNode, const FString TargetPathStr)
+	{
+		FEzAbilityPropertyPath SourcePath;
+		FEzAbilityPropertyPath TargetPath;
+		SourcePath.SetStructID(SourceNode.ID);
+		TargetPath.SetStructID(TargetNode.ID);
+		if (SourcePath.FromString(SourcePathStr) && TargetPath.FromString(TargetPathStr))
+		{
+			EditorBindings.AddPropertyBinding(SourcePath, TargetPath);
+			return true;
+		}
+		return false;
+	}
+
+#if WITH_EZABILITY_DEBUGGER
+	bool HasAnyBreakpoint(FGuid ID) const;
+	bool HasBreakpoint(FGuid ID, EEzAbilityBreakpointType BreakpointType) const;
+	const FEzAbilityEditorBreakpoint* GetBreakpoint(FGuid ID, EEzAbilityBreakpointType BreakpointType) const;
+	void AddBreakpoint(FGuid ID, EEzAbilityBreakpointType BreakpointType);
+	bool RemoveBreakpoint(FGuid ID, EEzAbilityBreakpointType BreakpointType);
+#endif // WITH_EZABILITY_DEBUGGER
+
+	// ~EzAbility Builder API
+
+	/**
+	 * Attempts to find a Color matching the provided Color Key
+	 */
+	const FEzAbilityEditorColor* FindColor(const FEzAbilityEditorColorRef& ColorRef) const
+	{
+		return Colors.Find(FEzAbilityEditorColor(ColorRef));
+	}
+
+private:
+	void FixObjectInstance(TSet<UObject*>& SeenObjects, UObject& Outer, FEzAbilityEditorNode& Node);
+	void FixObjectNodes();
+	void FixDuplicateIDs();
+	void UpdateBindingsInstanceStructs();
+
+#if WITH_EDITORONLY_DATA
+	FDelegateHandle OnObjectsReinstancedHandle;
+	FDelegateHandle OnUserDefinedStructReinstancedHandle;
+	FDelegateHandle OnParametersChangedHandle;
+#endif
+
+public:
+	/** Schema describing which inputs, evaluators, and tasks a EzAbility can contain */	
+	UPROPERTY(EditDefaultsOnly, Category = Common, Instanced)
+	TObjectPtr<UEzAbilitySchema> Schema = nullptr;
+
+	/** Public parameters that could be used for bindings within the Tree. */
+	UPROPERTY(EditDefaultsOnly, Category = Parameters)
+	FEzAbilityStateParameters RootParameters;
+
+	UPROPERTY(EditDefaultsOnly, Category = "Evaluators", meta = (BaseStruct = "/Script/EzAbilityModule.EzAbilityEvaluatorBase", BaseClass = "/Script/EzAbilityModule.EzAbilityEvaluatorBlueprintBase"))
+	TArray<FEzAbilityEditorNode> Evaluators;
+
+	UPROPERTY(EditDefaultsOnly, Category = "Global Tasks", meta = (BaseStruct = "/Script/EzAbilityModule.EzAbilityTaskBase", BaseClass = "/Script/EzAbilityModule.EzAbilityTaskBlueprintBase"))
+	TArray<FEzAbilityEditorNode> GlobalTasks;
+
+	UPROPERTY(meta = (ExcludeFromHash))
+	FEzAbilityEditorPropertyBindings EditorBindings;
+
+	/** Color Options to assign to a State */
+	UPROPERTY(EditDefaultsOnly, Category = "Theme")
+	TSet<FEzAbilityEditorColor> Colors;
+
+	/** Top level States. */
+	UPROPERTY()
+	TArray<TObjectPtr<UEzAbilityState>> SubTrees;
+
+	/**
+	 * Transient list of breakpoints added in the debugging session.
+	 * These will be lost if the asset gets reloaded.
+	 * If there is eventually a change to make those persist with the asset
+	 * we need to prune all dangling breakpoints after states/tasks got removed. (see RemoveUnusedBindings)
+	 */
+	UPROPERTY(Transient)
+	TArray<FEzAbilityEditorBreakpoint> Breakpoints;
 };

+ 79 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditorNode.h

@@ -0,0 +1,79 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "EzAbilityNodeBase.h"
+#include "EzAbilityEditorNode.generated.h"
+
+UENUM()
+enum class EEzAbilityNodeType : uint8
+{
+	EnterCondition,
+	Evaluator,
+	Task,
+	TransitionCondition,
+	StateParameters,
+};
+
+/**
+ * Base for Evaluator, Task and Condition nodes.
+ */
+USTRUCT()
+struct EZABILITYEDITOR_API FEzAbilityEditorNode
+{
+	GENERATED_BODY()
+
+	void Reset()
+	{
+		Node.Reset();
+		Instance.Reset();
+		InstanceObject = nullptr;
+		ID = FGuid();
+	}
+
+	FName GetName() const
+	{
+		if (const FEzAbilityNodeBase* NodePtr = Node.GetPtr<FEzAbilityNodeBase>())
+		{
+			return NodePtr->Name;
+		}
+		return FName();
+	}
+
+	const FEzAbilityDataView GetInstance() const
+	{
+		return InstanceObject ? FEzAbilityDataView(InstanceObject) : FEzAbilityDataView(const_cast<FInstancedStruct&>(Instance));
+	}
+
+	FEzAbilityDataView GetInstance()
+	{
+		return InstanceObject ? FEzAbilityDataView(InstanceObject) : FEzAbilityDataView(Instance);
+	}
+
+	UPROPERTY(EditDefaultsOnly, Category = Node)
+	FInstancedStruct Node;
+
+	UPROPERTY(EditDefaultsOnly, Category = Node)
+	FInstancedStruct Instance;
+
+	UPROPERTY(EditDefaultsOnly, Instanced, Category = Node)
+	TObjectPtr<UObject> InstanceObject = nullptr;
+	
+	UPROPERTY(EditDefaultsOnly, Category = Node)
+	FGuid ID;
+
+	UPROPERTY(EditDefaultsOnly, Category = Node)
+	uint8 ConditionIndent = 0;
+
+	UPROPERTY(EditDefaultsOnly, Category = Node)
+	EEzAbilityConditionOperand ConditionOperand = EEzAbilityConditionOperand::And; 
+};
+
+template <typename T>
+struct TEzAbilityEditorNode : public FEzAbilityEditorNode
+{
+	using NodeType = T;
+	FORCEINLINE T& GetNode() { return Node.template GetMutable<T>(); }
+	FORCEINLINE typename T::FInstanceDataType& GetInstanceData() { return Instance.template GetMutable<typename T::FInstanceDataType>(); }
+};
+

+ 124 - 123
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditorPropertyBindings.h

@@ -3,127 +3,128 @@
 #pragma once
 
 #include "CoreMinimal.h"
+#include "EzAbilityPropertyBindings.h"
 #include "UObject/Object.h"
-//#include "EzAbilityEditorPropertyBindings.generated.h"
-
-// /**
-//  * Editor representation of a all property bindings in a StateTree
-//  */
-// USTRUCT()
-// struct EZABILITYEDITOR_API FEzAbilityEditorPropertyBindings
-// {
-// 	GENERATED_BODY()
-//
-// 	/**
-// 	 * Adds binding between source and destination paths. Removes any bindings to TargetPath before adding the new one.
-// 	 * @param SourcePath Binding source property path.
-// 	 * @param TargetPath Binding target property path.
-// 	 */
-// 	void AddPropertyBinding(const FEzAbilityPropertyPath& SourcePath, const FEzAbilityPropertyPath& TargetPath);
-// 	
-// 	/**
-// 	 * Removes all bindings to target path.
-// 	 * @param TargetPath Target property path.
-// 	 */ 
-// 	void RemovePropertyBindings(const FEzAbilityPropertyPath& TargetPath);
-// 	
-// 	/**
-// 	 * @param TargetPath Target property path.
-// 	 * @return True of the target path has any bindings.
-// 	 */
-// 	bool HasPropertyBinding(const FEzAbilityPropertyPath& TargetPath) const;
-//
-// 	/**
-// 	 * Copies property bindings from an existing struct to another.
-// 	 * Overrides a binding to a specific property if it already exists in ToStructID.
-// 	 * @param FromStructID ID of the struct to copy from.
-// 	 * @param ToStructID ID of the struct to copy to.
-// 	 */
-// 	void CopyBindings(const FGuid FromStructID, const FGuid ToStructID);
-// 	
-// 	/**
-// 	 * @return Source path for given target path, or null if binding does not exists.
-// 	 */
-// 	const FEzAbilityPropertyPath* GetPropertyBindingSource(const FEzAbilityPropertyPath& TargetPath) const;
-// 	
-// 	/**
-// 	 * Returns all bindings for a specified structs based in struct ID.
-// 	 * @param StructID ID of the struct to find bindings for.
-// 	 * @param OutBindings Bindings for specified struct.
-// 	 */
-// 	void GetPropertyBindingsFor(const FGuid StructID, TArray<FEzAbilityPropertyPathBinding>& OutBindings) const;
-// 	
-// 	/**
-// 	 * Removes bindings which do not point to valid structs IDs.
-// 	 * @param ValidStructs Set of struct IDs that are currently valid.
-// 	 */
-// 	void RemoveUnusedBindings(const TMap<FGuid, const FStateTreeDataView>& ValidStructs);
-//
-// 	/** @return true if any of the bindings references any of the Structs. */
-// 	bool ContainsAnyStruct(const TSet<const UStruct*>& Structs);
-//
-// 	/** @return array view to all bindings. */
-// 	TConstArrayView<FEzAbilityPropertyPathBinding> GetBindings() const { return PropertyBindings; }
-//
-// 	TArrayView<FEzAbilityPropertyPathBinding> GetMutableBindings() { return PropertyBindings; }
-//
-// private:
-//
-// 	UPROPERTY()
-// 	TArray<FEzAbilityPropertyPathBinding> PropertyBindings;
-// };
-//
-//
-// UINTERFACE(MinimalAPI, meta = (CannotImplementInterfaceInBlueprint))
-// class UEzAbilityEditorPropertyBindingsOwner : public UInterface
-// {
-// 	GENERATED_UINTERFACE_BODY()
-// };
-//
-// class EZABILITYEDITOR_API IEzAbilityEditorPropertyBindingsOwner
-// {
-// 	GENERATED_IINTERFACE_BODY()
-//
-// 	/**
-// 	 * Returns structs within the owner that are visible for target struct.
-// 	 * @param TargetStructID Target struct ID
-// 	 * @param OutStructDescs Result descriptors of the visible structs.
-// 	 */
-// 	virtual void GetAccessibleStructs(const FGuid TargetStructID, TArray<FEzAbilityBindableStructDesc>& OutStructDescs) const PURE_VIRTUAL(IEzAbilityEditorPropertyBindingsOwner::GetAccessibleStructs, return; );
-//
-// 	/**
-// 	 * Returns struct descriptor based on struct ID.
-// 	 * @param StructID Target struct ID
-// 	 * @param OutStructDesc Result descriptor.
-// 	 * @return True if struct found.
-// 	 */
-// 	virtual bool GetStructByID(const FGuid StructID, FEzAbilityBindableStructDesc& OutStructDesc) const PURE_VIRTUAL(IEzAbilityEditorPropertyBindingsOwner::GetStructByID, return false; );
-//
-// 	/**
-// 	 * Returns data view based on struct ID.
-// 	 * @param StructID Target struct ID
-// 	 * @param OutDataView Result data view.
-// 	 * @return True if struct found.
-// 	 */
-// 	virtual bool GetDataViewByID(const FGuid StructID, FEzAbilityDataView& OutDataView) const PURE_VIRTUAL(IEzAbilityEditorPropertyBindingsOwner::GetDataViewByID, return false; );
-//
-// 	/** @return Pointer to editor property bindings. */
-// 	virtual FEzAbilityEditorPropertyBindings* GetPropertyEditorBindings() PURE_VIRTUAL(IEzAbilityEditorPropertyBindingsOwner::GetPropertyEditorBindings, return nullptr; );
-// };
-//
-// // TODO: We should merge this with IEzAbilityEditorPropertyBindingsOwner and FEzAbilityEditorPropertyBindings.
-// // Currently FStateTreeEditorPropertyBindings is meant to be used as a member for just to store things,
-// // IStateTreeEditorPropertyBindingsOwner is meant return model specific stuff,
-// // and IStateTreeBindingLookup is used in non-editor code and it cannot be in FStateTreeEditorPropertyBindings because bindings don't know about the owner.
-// struct EZABILITYEDITOR_API FEzAbilityBindingLookup : public IEzAbilityBindingLookup
-// {
-// 	FEzAbilityBindingLookup(IEzAbilityEditorPropertyBindingsOwner* InBindingOwner);
-//
-// 	IEzAbilityEditorPropertyBindingsOwner* BindingOwner = nullptr;
-//
-// protected:
-// 	virtual const FEzAbilityPropertyPath* GetPropertyBindingSource(const FEzAbilityPropertyPath& InTargetPath) const override;
-// 	virtual FText GetPropertyPathDisplayName(const FEzAbilityPropertyPath& InTargetPath) const override;
-// 	virtual const FProperty* GetPropertyPathLeafProperty(const FEzAbilityPropertyPath& InPath) const override;
-//
-// };
+#include "EzAbilityEditorPropertyBindings.generated.h"
+
+ /**
+  * Editor representation of a all property bindings in a EzAbility
+  */
+ USTRUCT()
+ struct EZABILITYEDITOR_API FEzAbilityEditorPropertyBindings
+ {
+ 	GENERATED_BODY()
+
+ 	/**
+ 	 * Adds binding between source and destination paths. Removes any bindings to TargetPath before adding the new one.
+ 	 * @param SourcePath Binding source property path.
+ 	 * @param TargetPath Binding target property path.
+ 	 */
+ 	void AddPropertyBinding(const FEzAbilityPropertyPath& SourcePath, const FEzAbilityPropertyPath& TargetPath);
+ 	
+ 	/**
+ 	 * Removes all bindings to target path.
+ 	 * @param TargetPath Target property path.
+ 	 */ 
+ 	void RemovePropertyBindings(const FEzAbilityPropertyPath& TargetPath);
+ 	
+ 	/**
+ 	 * @param TargetPath Target property path.
+ 	 * @return True of the target path has any bindings.
+ 	 */
+ 	bool HasPropertyBinding(const FEzAbilityPropertyPath& TargetPath) const;
+
+ 	/**
+ 	 * Copies property bindings from an existing struct to another.
+ 	 * Overrides a binding to a specific property if it already exists in ToStructID.
+ 	 * @param FromStructID ID of the struct to copy from.
+ 	 * @param ToStructID ID of the struct to copy to.
+ 	 */
+ 	void CopyBindings(const FGuid FromStructID, const FGuid ToStructID);
+ 	
+ 	/**
+ 	 * @return Source path for given target path, or null if binding does not exists.
+ 	 */
+ 	const FEzAbilityPropertyPath* GetPropertyBindingSource(const FEzAbilityPropertyPath& TargetPath) const;
+ 	
+ 	/**
+ 	 * Returns all bindings for a specified structs based in struct ID.
+ 	 * @param StructID ID of the struct to find bindings for.
+ 	 * @param OutBindings Bindings for specified struct.
+ 	 */
+ 	void GetPropertyBindingsFor(const FGuid StructID, TArray<FEzAbilityPropertyPathBinding>& OutBindings) const;
+ 	
+ 	/**
+ 	 * Removes bindings which do not point to valid structs IDs.
+ 	 * @param ValidStructs Set of struct IDs that are currently valid.
+ 	 */
+ 	void RemoveUnusedBindings(const TMap<FGuid, const FEzAbilityDataView>& ValidStructs);
+
+ 	/** @return true if any of the bindings references any of the Structs. */
+ 	bool ContainsAnyStruct(const TSet<const UStruct*>& Structs);
+
+ 	/** @return array view to all bindings. */
+ 	TConstArrayView<FEzAbilityPropertyPathBinding> GetBindings() const { return PropertyBindings; }
+
+ 	TArrayView<FEzAbilityPropertyPathBinding> GetMutableBindings() { return PropertyBindings; }
+
+ private:
+
+ 	UPROPERTY()
+ 	TArray<FEzAbilityPropertyPathBinding> PropertyBindings;
+ };
+
+
+ UINTERFACE(MinimalAPI, meta = (CannotImplementInterfaceInBlueprint))
+ class UEzAbilityEditorPropertyBindingsOwner : public UInterface
+ {
+ 	GENERATED_UINTERFACE_BODY()
+ };
+
+ class EZABILITYEDITOR_API IEzAbilityEditorPropertyBindingsOwner
+ {
+ 	GENERATED_IINTERFACE_BODY()
+
+ 	/**
+ 	 * Returns structs within the owner that are visible for target struct.
+ 	 * @param TargetStructID Target struct ID
+ 	 * @param OutStructDescs Result descriptors of the visible structs.
+ 	 */
+ 	virtual void GetAccessibleStructs(const FGuid TargetStructID, TArray<FEzAbilityBindableStructDesc>& OutStructDescs) const PURE_VIRTUAL(IEzAbilityEditorPropertyBindingsOwner::GetAccessibleStructs, return; );
+
+ 	/**
+ 	 * Returns struct descriptor based on struct ID.
+ 	 * @param StructID Target struct ID
+ 	 * @param OutStructDesc Result descriptor.
+ 	 * @return True if struct found.
+ 	 */
+ 	virtual bool GetStructByID(const FGuid StructID, FEzAbilityBindableStructDesc& OutStructDesc) const PURE_VIRTUAL(IEzAbilityEditorPropertyBindingsOwner::GetStructByID, return false; );
+
+ 	/**
+ 	 * Returns data view based on struct ID.
+ 	 * @param StructID Target struct ID
+ 	 * @param OutDataView Result data view.
+ 	 * @return True if struct found.
+ 	 */
+ 	virtual bool GetDataViewByID(const FGuid StructID, FEzAbilityDataView& OutDataView) const PURE_VIRTUAL(IEzAbilityEditorPropertyBindingsOwner::GetDataViewByID, return false; );
+
+ 	/** @return Pointer to editor property bindings. */
+ 	virtual FEzAbilityEditorPropertyBindings* GetPropertyEditorBindings() PURE_VIRTUAL(IEzAbilityEditorPropertyBindingsOwner::GetPropertyEditorBindings, return nullptr; );
+ };
+
+ // TODO: We should merge this with IEzAbilityEditorPropertyBindingsOwner and FEzAbilityEditorPropertyBindings.
+ // Currently FEzAbilityEditorPropertyBindings is meant to be used as a member for just to store things,
+ // IEzAbilityEditorPropertyBindingsOwner is meant return model specific stuff,
+ // and IEzAbilityBindingLookup is used in non-editor code and it cannot be in FEzAbilityEditorPropertyBindings because bindings don't know about the owner.
+ struct EZABILITYEDITOR_API FEzAbilityBindingLookup : public IEzAbilityBindingLookup
+ {
+ 	FEzAbilityBindingLookup(IEzAbilityEditorPropertyBindingsOwner* InBindingOwner);
+
+ 	IEzAbilityEditorPropertyBindingsOwner* BindingOwner = nullptr;
+
+ protected:
+ 	virtual const FEzAbilityPropertyPath* GetPropertyBindingSource(const FEzAbilityPropertyPath& InTargetPath) const override;
+ 	virtual FText GetPropertyPathDisplayName(const FEzAbilityPropertyPath& InTargetPath) const override;
+ 	virtual const FProperty* GetPropertyPathLeafProperty(const FEzAbilityPropertyPath& InPath) const override;
+
+ };

+ 95 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityEditorTypes.h

@@ -0,0 +1,95 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "Containers/UnrealString.h"
+#include "Math/Color.h"
+#include "Misc/Guid.h"
+#include "UObject/Class.h"
+#include "EzAbilityEditorTypes.generated.h"
+
+/**
+ * Id Struct to uniquely identify an FEzAbilityEditorColor instance.
+ * An existing FEzAbilityEditorColor instance can be found via UEzAbilityEditorData::FindColor
+ */
+USTRUCT()
+struct FEzAbilityEditorColorRef
+{
+	GENERATED_BODY()
+
+	FEzAbilityEditorColorRef() = default;
+
+	explicit FEzAbilityEditorColorRef(const FGuid& ID)
+		: ID(ID)
+	{
+	}
+
+	bool operator==(const FEzAbilityEditorColorRef& Other) const
+	{
+		return ID == Other.ID;
+	}
+
+	friend uint32 GetTypeHash(const FEzAbilityEditorColorRef& ColorRef)
+	{
+		return GetTypeHash(ColorRef.ID);
+	}
+
+	UPROPERTY(EditDefaultsOnly, Category = "Theme")
+	FGuid ID;
+};
+
+/**
+ * Struct describing a Color, its display name and a unique identifier to get an instance via UEzAbilityEditorData::FindColor
+ */
+USTRUCT()
+struct FEzAbilityEditorColor
+{
+	GENERATED_BODY()
+
+	FEzAbilityEditorColor()
+		: ColorRef(FGuid::NewGuid())
+	{
+	}
+
+	explicit FEzAbilityEditorColor(const FEzAbilityEditorColorRef& ColorRef)
+		: ColorRef(ColorRef)
+	{
+	}
+
+	/**
+	 * Export Text Item override where properties marked with meta-data "StructExportTransient" are excluded from the exported string
+	 * This is so that copy/pasting State Tree Color entries don't have the effect of also copying over these properties into a new entry.
+	 * Since it works with meta-data, it's editor-only.
+	 * Side note: The existing "TextExportTransient" / "DuplicateTransient" specifiers apply for uclass properties only
+	 */ 
+	EZABILITYEDITOR_API bool ExportTextItem(FString& OutValueString, const FEzAbilityEditorColor& DefaultValue, UObject* OwnerObject, int32 PortFlags, UObject* ExportRootScope) const;
+
+	bool operator==(const FEzAbilityEditorColor& Other) const
+	{
+		return ColorRef == Other.ColorRef;
+	}
+
+	friend uint32 GetTypeHash(const FEzAbilityEditorColor& InColor)
+	{
+		return GetTypeHash(InColor.ColorRef);
+	}
+
+	/** ID unique per State Tree Color Entry. Marked as struct export transient so that copy-pasting this entry does not result in the same repeating ID */
+	UPROPERTY(meta=(StructExportTransient))
+	FEzAbilityEditorColorRef ColorRef;
+
+	UPROPERTY(EditDefaultsOnly, Category = "Theme")
+	FString DisplayName;
+
+	UPROPERTY(EditDefaultsOnly, Category = "Theme", meta=(HideAlphaChannel))
+	FLinearColor Color = FLinearColor(0.4f, 0.4f, 0.4f);
+};
+
+template<>
+struct TStructOpsTypeTraits<FEzAbilityEditorColor> : TStructOpsTypeTraitsBase2<FEzAbilityEditorColor>
+{
+	enum
+	{
+		WithExportTextItem = true,
+	};
+};

+ 198 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityPropertyHelpers.h

@@ -0,0 +1,198 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "Editor.h"
+#include "PropertyHandle.h"
+
+struct FGuid;
+struct FSlateBrush;
+
+#define LOCTEXT_NAMESPACE "EzAbilityEditor"
+
+namespace UE::EzAbility::PropertyHelpers {
+
+/**
+ * Dispatches PostEditChange to all FState
+ * Assumes property chain head is member property of Owner. 
+ */
+void DispatchPostEditToNodes(UObject& Owner, FPropertyChangedChainEvent& PropertyChangedEvent);
+
+/** Makes deterministic ID from the owners property path, a property path (or any string), and a seed value (e.g. array index). */
+FGuid MakeDeterministicID(const UObject& Owner, const FString& PropertyPath, const uint64 Seed);
+
+/* @return true if the property handle points to struct property of specified type.*/
+template<typename T>
+bool IsScriptStruct(const TSharedPtr<IPropertyHandle>& PropertyHandle)
+{
+	if (!PropertyHandle)
+	{
+		return false;
+	}
+
+	FStructProperty* StructProperty = CastField<FStructProperty>(PropertyHandle->GetProperty());
+	return StructProperty && StructProperty->Struct->IsA(TBaseStructure<T>::Get()->GetClass());
+}
+/**
+ * @return true if provided Property contains "Optional" metadata
+ */
+bool HasOptionalMetadata(const FProperty& Property);
+
+/**
+ * Gets a struct value from property handle, checks type before access. Expects T is struct.
+ * @param ValueProperty Handle to property where value is got from.
+ * @return Requested value as optional, in case of multiple values the optional is unset.
+ */
+template<typename T>
+FPropertyAccess::Result GetStructValue(const TSharedPtr<const IPropertyHandle>& ValueProperty, T& OutValue)
+{
+	if (!ValueProperty)
+	{
+		return FPropertyAccess::Fail;
+	}
+
+	FStructProperty* StructProperty = CastFieldChecked<FStructProperty>(ValueProperty->GetProperty());
+	check(StructProperty);
+	check(StructProperty->Struct == TBaseStructure<T>::Get());
+
+	T Value = T();
+	bool bValueSet = false;
+
+	TArray<const void*> RawData;
+	ValueProperty->AccessRawData(RawData);
+	for (const void* Data : RawData)
+	{
+		if (Data)
+		{
+			const T& CurValue = *static_cast<const T*>(Data);
+			if (!bValueSet)
+			{
+				bValueSet = true;
+				Value = CurValue;
+			}
+			else if (CurValue != Value)
+			{
+				// Multiple values
+				return FPropertyAccess::MultipleValues;
+			}
+		}
+	}
+
+	OutValue = Value;
+
+	return FPropertyAccess::Success;
+}
+
+/**
+ * Sets a struct property to specific value, checks type before access. Expects T is struct.
+ * @param ValueProperty Handle to property where value is got from.
+ * @return Requested value as optional, in case of multiple values the optional is unset.
+ */
+template<typename T>
+FPropertyAccess::Result SetStructValue(const TSharedPtr<IPropertyHandle>& ValueProperty, const T& NewValue, EPropertyValueSetFlags::Type Flags = 0)
+{
+	if (!ValueProperty)
+	{
+		return FPropertyAccess::Fail;
+	}
+
+	FStructProperty* StructProperty = CastFieldChecked<FStructProperty>(ValueProperty->GetProperty());
+	if (!StructProperty)
+	{
+		return FPropertyAccess::Fail;
+	}
+	if (StructProperty->Struct != TBaseStructure<T>::Get())
+	{
+		return FPropertyAccess::Fail;
+	}
+
+	const bool bTransactable = (Flags & EPropertyValueSetFlags::NotTransactable) == 0;
+	bool bNotifiedPreChange = false;
+	TArray<void*> RawData;
+	ValueProperty->AccessRawData(RawData);
+	for (void* Data : RawData)
+	{
+		if (Data)
+		{
+			if (!bNotifiedPreChange)
+			{
+				if (bTransactable && GEditor)
+				{
+					GEditor->BeginTransaction(FText::Format(LOCTEXT("SetPropertyValue", "Set {0}"), ValueProperty->GetPropertyDisplayName()));
+				}
+				ValueProperty->NotifyPreChange();
+				bNotifiedPreChange = true;
+			}
+
+			T* Value = reinterpret_cast<T*>(Data);
+			*Value = NewValue;
+		}
+	}
+
+	if (bNotifiedPreChange)
+	{
+		ValueProperty->NotifyPostChange(EPropertyChangeType::ValueSet);
+		if (bTransactable && GEditor)
+		{
+			GEditor->EndTransaction();
+		}
+	}
+
+	ValueProperty->NotifyFinishedChangingProperties();
+
+	return FPropertyAccess::Success;
+}
+
+}; // UE::EzAbility::PropertyHelpers
+
+/**
+ * Helper class to deal with relative property paths in PostEditChangeChainProperty().
+ */
+struct FEzAbilityEditPropertyPath
+{
+private:
+	struct FEzAbilityEditPropertySegment
+	{
+		FEzAbilityEditPropertySegment() = default;
+		FEzAbilityEditPropertySegment(const FProperty* InProperty, const FName InPropertyName, const int32 InArrayIndex = INDEX_NONE)
+			: Property(InProperty)
+			, PropertyName(InPropertyName)
+			, ArrayIndex(InArrayIndex)
+		{
+		}
+	
+		const FProperty* Property = nullptr;
+		FName PropertyName = FName();
+		int32 ArrayIndex = INDEX_NONE;
+	};
+	
+public:
+	FEzAbilityEditPropertyPath() = default;
+
+	/** Makes property path relative to BaseStruct. Checks if the path is not part of the type. */
+	explicit FEzAbilityEditPropertyPath(const UStruct* BaseStruct, const FString& InPath);
+
+	/** Makes property path from property change event. */
+	explicit FEzAbilityEditPropertyPath(const FPropertyChangedChainEvent& PropertyChangedEvent);
+
+	/** Makes property path from property chain. */
+	explicit FEzAbilityEditPropertyPath(const FEditPropertyChain& PropertyChain);
+
+	/** @return true if the property path contains specified path. */
+	bool ContainsPath(const FEzAbilityEditPropertyPath& InPath) const;
+
+	/** @return true if the property path is exactly the specified path. */
+	bool IsPathExact(const FEzAbilityEditPropertyPath& InPath) const;
+
+	/** @return array index at specified property, or INDEX_NONE, if the property is not array or property not found.  */
+	int32 GetPropertyArrayIndex(const FEzAbilityEditPropertyPath& InPath) const
+	{
+		return ContainsPath(InPath) ? Path[InPath.Path.Num() - 1].ArrayIndex : INDEX_NONE;
+	}
+
+private:
+	TArray<FEzAbilityEditPropertySegment> Path;
+};
+
+#undef LOCTEXT_NAMESPACE
+

+ 274 - 0
Ability/Plugins/EzAbility/Source/EzAbilityEditor/Public/EzAbilityState.h

@@ -0,0 +1,274 @@
+// Fill out your copyright notice in the Description page of Project Settings.
+
+#pragma once
+
+#include "EzAbilityEditorNode.h"
+#include "EzAbilityEditorTypes.h"
+#include "EzAbilityState.generated.h"
+
+class UEzAbilityState;
+
+/**
+ * Editor representation of a transition in EzAbility
+ */
+USTRUCT()
+struct EZABILITYEDITOR_API FEzAbilityTransition
+{
+	GENERATED_BODY()
+
+	FEzAbilityTransition() = default;
+	FEzAbilityTransition(const EEzAbilityTransitionTrigger InTrigger, const EEzAbilityTransitionType InType, const UEzAbilityState* InState = nullptr);
+	FEzAbilityTransition(const EEzAbilityTransitionTrigger InTrigger, const FGameplayTag InEventTag, const EEzAbilityTransitionType InType, const UEzAbilityState* InState = nullptr);
+
+	template<typename T, typename... TArgs>
+	TEzAbilityEditorNode<T>& AddCondition(TArgs&&... InArgs)
+	{
+		FEzAbilityEditorNode& CondNode = Conditions.AddDefaulted_GetRef();
+		CondNode.ID = FGuid::NewGuid();
+		CondNode.Node.InitializeAs<T>(Forward<TArgs>(InArgs)...);
+		T& Cond = CondNode.Node.GetMutable<T>();
+		if (const UScriptStruct* InstanceType = Cast<const UScriptStruct>(Cond.GetInstanceDataType()))
+		{
+			CondNode.Instance.InitializeAs(InstanceType);
+		}
+		return static_cast<TEzAbilityEditorNode<T>&>(CondNode);
+	}
+
+	/** When to try trigger the transition. */
+	UPROPERTY(EditDefaultsOnly, Category = "Transition")
+	EEzAbilityTransitionTrigger Trigger = EEzAbilityTransitionTrigger::OnStateCompleted;
+
+	/** Tag of the State Tree event that triggers the transition. */
+	UPROPERTY(EditDefaultsOnly, Category = "Transition")
+	FGameplayTag EventTag;
+
+	/** Transition target state. */
+	UPROPERTY(EditDefaultsOnly, Category = "Transition", meta=(DisplayName="Transition To"))
+	FEzAbilityStateLink State;
+
+	UPROPERTY(EditDefaultsOnly, Category = "Transition")
+	FGuid ID;
+
+	/**
+	 * Transition priority when multiple transitions happen at the same time.
+	 * During transition handling, the transitions are visited from leaf to root.
+	 * The first visited transition, of highest priority, that leads to a state selection, will be activated.
+	 */
+	UPROPERTY(EditDefaultsOnly, Category = "Transition")
+	EEzAbilityTransitionPriority Priority = EEzAbilityTransitionPriority::Normal;
+
+	/** Delay the triggering of the transition. */
+	UPROPERTY(EditDefaultsOnly, Category = "Transition")
+	bool bDelayTransition = false;
+
+	/** Transition delay duration in seconds. */
+	UPROPERTY(EditDefaultsOnly, Category = "Transition", meta = (EditCondition = "bDelayTransition", UIMin = "0", ClampMin = "0", UIMax = "25", ClampMax = "25", ForceUnits="s"))
+	float DelayDuration = 0.0f;
+
+	/** Transition delay random variance in seconds. */
+	UPROPERTY(EditDefaultsOnly, Category = "Transition", meta = (EditCondition = "bDelayTransition", UIMin = "0", ClampMin = "0", UIMax = "25", ClampMax = "25", ForceUnits="s"))
+	float DelayRandomVariance = 0.0f;
+
+	/** Conditions that must pass so that the transition can be triggered. */
+	UPROPERTY(EditDefaultsOnly, Category = "Transition", meta = (BaseStruct = "/Script/EzAbilityModule.EzAbilityConditionBase", BaseClass = "/Script/EzAbilityModule.EzAbilityConditionBlueprintBase"))
+	TArray<FEzAbilityEditorNode> Conditions;
+
+	/** True if the Transition is Enabled (i.e. not explicitly disabled in the asset). */
+	UPROPERTY(EditDefaultsOnly, Category = "Debug")
+	bool bTransitionEnabled = true;
+};
+
+
+USTRUCT()
+struct EZABILITYEDITOR_API FEzAbilityStateParameters
+{
+	GENERATED_BODY()
+
+	void Reset()
+	{
+		Parameters.Reset();
+		PropertyOverrides.Reset();
+		bFixedLayout = false;
+	}
+
+	/** Removes overrides that do appear in Parameters. */
+	void RemoveUnusedOverrides();
+	
+	UPROPERTY(EditDefaultsOnly, Category = Parameters)
+	FInstancedPropertyBag Parameters;
+
+	/** Overrides for parameters. */
+	UPROPERTY()
+	TArray<FGuid> PropertyOverrides;
+
+	UPROPERTY(EditDefaultsOnly, Category = Parameters)
+	bool bFixedLayout = false;
+
+	UPROPERTY(EditDefaultsOnly, Category = Parameters, meta = (IgnoreForMemberInitializationTest))
+	FGuid ID;
+};
+
+/**
+ * Editor representation of a state in EzAbility
+ */
+UCLASS(BlueprintType, EditInlineNew, CollapseCategories)
+class EZABILITYEDITOR_API UEzAbilityState : public UObject
+{
+	GENERATED_BODY()
+
+public:
+	UEzAbilityState(const FObjectInitializer& ObjectInitializer);
+	virtual ~UEzAbilityState() override;
+
+	virtual void PostInitProperties() override;
+	virtual void PreEditChange(FEditPropertyChain& PropertyAboutToChange) override;
+	virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override;
+	virtual void PostLoad() override;;
+	void UpdateParametersFromLinkedSubtree();
+	void OnTreeCompiled(const UEzAbility& EzAbility);
+
+	const UEzAbilityState* GetRootState() const;
+	const UEzAbilityState* GetNextSiblingState() const;
+	const UEzAbilityState* GetNextSelectableSiblingState() const;
+
+	/** @return true if the property of specified ID is overridden. */
+	bool IsParametersPropertyOverridden(const FGuid PropertyID) const
+	{
+		return Parameters.PropertyOverrides.Contains(PropertyID);
+	}
+
+	/** Sets the override status of specified property by ID. */
+	void SetParametersPropertyOverridden(const FGuid PropertyID, const bool bIsOverridden);
+
+	/** @returns Default parameters from linked state or asset). */
+	const FInstancedPropertyBag* GetDefaultParameters() const;
+	
+	// EzAbility Builder API
+	/** @return state link to this state. */
+	FEzAbilityStateLink GetLinkToState() const;
+	
+	/** Adds child state with specified name. */
+	UEzAbilityState& AddChildState(const FName ChildName, const EEzAbilityStateType StateType = EEzAbilityStateType::State)
+	{
+		UEzAbilityState* ChildState = NewObject<UEzAbilityState>(this, FName(), RF_Transactional);
+		check(ChildState);
+		ChildState->Name = ChildName;
+		ChildState->Parent = this;
+		ChildState->Type = StateType;
+		Children.Add(ChildState);
+		return *ChildState;
+	}
+
+	/**
+	 * Adds enter condition of specified type.
+	 * @return reference to the new condition.
+	 */
+	template<typename T, typename... TArgs>
+	TEzAbilityEditorNode<T>& AddEnterCondition(TArgs&&... InArgs)
+	{
+		FEzAbilityEditorNode& CondNode = EnterConditions.AddDefaulted_GetRef();
+		CondNode.ID = FGuid::NewGuid();
+		CondNode.Node.InitializeAs<T>(Forward<TArgs>(InArgs)...);
+		T& Cond = CondNode.Node.GetMutable<T>();
+		if (const UScriptStruct* InstanceType = Cast<const UScriptStruct>(Cond.GetInstanceDataType()))
+		{
+			CondNode.Instance.InitializeAs(InstanceType);
+		}
+		return static_cast<TEzAbilityEditorNode<T>&>(CondNode);
+	}
+
+	/**
+	 * Adds Task of specified type.
+	 * @return reference to the new Task.
+	 */
+	template<typename T, typename... TArgs>
+	TEzAbilityEditorNode<T>& AddTask(TArgs&&... InArgs)
+	{
+		FEzAbilityEditorNode& TaskItem = Tasks.AddDefaulted_GetRef();
+		TaskItem.ID = FGuid::NewGuid();
+		TaskItem.Node.InitializeAs<T>(Forward<TArgs>(InArgs)...);
+		T& Task = TaskItem.Node.GetMutable<T>();
+		if (const UScriptStruct* InstanceType = Cast<const UScriptStruct>(Task.GetInstanceDataType()))
+		{
+			TaskItem.Instance.InitializeAs(InstanceType);
+		}
+		return static_cast<TEzAbilityEditorNode<T>&>(TaskItem);
+	}
+
+	/**
+	 * Adds Transition.
+	 * @return reference to the new Transition.
+	 */
+	FEzAbilityTransition& AddTransition(const EEzAbilityTransitionTrigger InTrigger, const EEzAbilityTransitionType InType, const UEzAbilityState* InState = nullptr)
+	{
+		FEzAbilityTransition& Transition = Transitions.Emplace_GetRef(InTrigger, InType, InState);
+		Transition.ID = FGuid::NewGuid();
+		return Transition;
+	}
+
+	FEzAbilityTransition& AddTransition(const EEzAbilityTransitionTrigger InTrigger, const FGameplayTag InEventTag, const EEzAbilityTransitionType InType, const UEzAbilityState* InState = nullptr)
+	{
+		FEzAbilityTransition& Transition = Transitions.Emplace_GetRef(InTrigger, InEventTag, InType, InState);
+		Transition.ID = FGuid::NewGuid();
+		return Transition;
+	}
+
+
+	// ~EzAbility Builder API
+
+	/** Display name of the State */
+	UPROPERTY(EditDefaultsOnly, Category = "State")
+	FName Name;
+
+	/** Display color of the State */
+	UPROPERTY(EditDefaultsOnly, Category = "State", DisplayName = "Color")
+	FEzAbilityEditorColorRef ColorRef;
+
+	/** Type the State, allows e.g. states to be linked to other States. */
+	UPROPERTY(EditDefaultsOnly, Category = "State")
+	EEzAbilityStateType Type = EEzAbilityStateType::State;
+
+	/** How to treat child states when this State is selected.  */
+	UPROPERTY(EditDefaultsOnly, Category = "State")
+	EEzAbilityStateSelectionBehavior SelectionBehavior = EEzAbilityStateSelectionBehavior::TrySelectChildrenInOrder;
+
+	/** Subtree to run as extension of this State. */
+	UPROPERTY(EditDefaultsOnly, Category = "State", Meta=(DirectStatesOnly, SubtreesOnly))
+	FEzAbilityStateLink LinkedSubtree;
+
+	/** Another State Tree asset to run as extension of this State. */
+	UPROPERTY(EditDefaultsOnly, Category = "State")
+	TObjectPtr<UEzAbility> LinkedAsset = nullptr;
+
+	/** Parameters of this state. If the state is linked to another state or asset, the parameters are for the linked state. */
+	UPROPERTY(EditDefaultsOnly, Category = "State")
+	FEzAbilityStateParameters Parameters;
+
+	UPROPERTY(EditDefaultsOnly, Category = "State", meta = (IgnoreForMemberInitializationTest))
+	FGuid ID;
+
+	UPROPERTY(EditDefaultsOnly, Category = "Enter Conditions", meta = (BaseStruct = "/Script/EzAbilityModule.EzAbilityConditionBase", BaseClass = "/Script/EzAbilityModule.EzAbilityConditionBlueprintBase"))
+	TArray<FEzAbilityEditorNode> EnterConditions;
+
+	UPROPERTY(EditDefaultsOnly, Category = "Tasks", meta = (BaseStruct = "/Script/EzAbilityModule.EzAbilityTaskBase", BaseClass = "/Script/EzAbilityModule.EzAbilityTaskBlueprintBase"))
+	TArray<FEzAbilityEditorNode> Tasks;
+
+	// Single item used when schema calls for single task per state.
+	UPROPERTY(EditDefaultsOnly, Category = "Task", meta = (BaseStruct = "/Script/EzAbilityModule.EzAbilityTaskBase", BaseClass = "/Script/EzAbilityModule.EzAbilityTaskBlueprintBase"))
+	FEzAbilityEditorNode SingleTask;
+
+	UPROPERTY(EditDefaultsOnly, Category = "Transitions")
+	TArray<FEzAbilityTransition> Transitions;
+
+	UPROPERTY()
+	TArray<TObjectPtr<UEzAbilityState>> Children;
+
+	UPROPERTY(meta = (ExcludeFromHash))
+	bool bExpanded = true;
+
+	UPROPERTY(EditDefaultsOnly, Category = "State")
+	bool bEnabled = true;
+
+	UPROPERTY(meta = (ExcludeFromHash))
+	TObjectPtr<UEzAbilityState> Parent = nullptr;
+};

+ 46 - 6
Ability/Plugins/EzAbility/Source/EzAbilityTest/Private/EzAbilityTest.cpp

@@ -5,6 +5,15 @@
 
 #include "AITestsCommon.h"
 #include "EzAbility.h"
+#include "EzAbilityComponent.h"
+#include "EzAbilityEditorData.h"
+#include "EzAblityTestTypes.h"
+
+#include UE_INLINE_GENERATED_CPP_BY_NAME(EzAbilityTest)
+
+#define LOCTEXT_NAMESPACE "AITestSuite_EzAbilityTest"
+
+UE_DISABLE_OPTIMIZATION_SHIP
 
 namespace UE::EzAbility::Tests
 {
@@ -12,10 +21,10 @@ namespace UE::EzAbility::Tests
 	{
 		UEzAbility* Ability = NewObject<UEzAbility>(Outer);
 		check(Ability);
-		// UStateTreeEditorData* EditorData = NewObject<UStateTreeEditorData>(Ability);
-		// check(EditorData);
-		// StateTree->EditorData = EditorData;
-		// EditorData->Schema = NewObject<UStateTreeTestSchema>();
+		UEzAbilityEditorData* EditorData = NewObject<UEzAbilityEditorData>(Ability);
+		check(EditorData);
+		Ability->EditorData = EditorData;
+		EditorData->Schema = NewObject<UEzAbilityTestSchema>();
 		return *Ability;
 	}
 }
@@ -34,12 +43,43 @@ IMPLEMENT_AI_INSTANT_TEST(FEzAbilityTest_MakeAndBakeEzAbility, "System.EzAbility
 
 //////////////////////////////////////////////////////////////////////////////////////////////////////////
 ///
-struct FEzAbilityTest_EmptyStateTree : FAITestBase
+struct FEzAbilityTest_EmptyAbility : FAITestBase
 {
 	virtual bool InstantTest() override
 	{
+		UEzAbility& Ability = UE::EzAbility::Tests::NewAbility(&GetWorld());
+		UEzAbilityEditorData& EditorData = *Cast<UEzAbilityEditorData>(Ability.EditorData);
+
+		UEzAbilityState& Root = EditorData.AddSubTree(FName(TEXT("Root")));
+		Root.AddTransition(EEzAbilityTransitionTrigger::OnStateCompleted, EEzAbilityTransitionType::Succeeded);
+
+		// FEzAbilityCompilerLog Log;
+		// FEzAbilityCompiler Compiler(Log);
+		// const bool bResult = Compiler.Compile(EzAbility);
+		//
+		// AITEST_TRUE("EzAbility should get compiled", bResult);
+		//
+		EAbilityRunStatus Status = EAbilityRunStatus::Unset;
+		FEzAbilityInstanceData InstanceData;
+		FTestEzAbilityExecutionContext Exec(Ability, Ability, InstanceData);
+		const bool bInitSucceeded = Exec.IsValid();
+		AITEST_TRUE("EzAbility should init", bInitSucceeded);
+
+		FEzAbilityParameter Parameter;
+		FText OutText;
+		Status = Exec.Start(&Ability, Parameter, OutText);
+		AITEST_TRUE("EzAbility should be running", Status == EAbilityRunStatus::Running);
+		Exec.LogClear();
+		
+		Status = Exec.Tick(0.1f);
+		AITEST_TRUE("EzAbility should be completed", Status == EAbilityRunStatus::Succeeded);
+		Exec.LogClear();
+		
 		return true;
 	}
 };
 
-IMPLEMENT_AI_INSTANT_TEST(FEzAbilityTest_EmptyStateTree, "System.EzAbility.EmptyAbility");
+IMPLEMENT_AI_INSTANT_TEST(FEzAbilityTest_EmptyAbility, "System.EzAbility.EmptyAbility");
+
+UE_ENABLE_OPTIMIZATION_SHIP
+#undef LOCTEXT_NAMESPACE

+ 7 - 8
Ability/Plugins/EzAbility/Source/EzAbilityTest/Private/EzAbilityTest.h

@@ -2,15 +2,14 @@
 
 #pragma once
 
-#include "CoreMinimal.h"
-#include "UObject/Object.h"
+#include "EzAbilitySchema.h"
 #include "EzAbilityTest.generated.h"
 
-/**
- * 
- */
-UCLASS()
-class EZABILITYTEST_API UEzAbilityTest : public UObject
+
+UCLASS(HideDropdown)
+class UEzAbilityTestSchema : public UEzAbilitySchema
 {
 	GENERATED_BODY()
-};
+
+	virtual bool IsStructAllowed(const UScriptStruct* InScriptStruct) const override { return true; }
+};

+ 521 - 0
Ability/Plugins/EzAbility/Source/EzAbilityTest/Private/EzAblityTestTypes.h

@@ -0,0 +1,521 @@
+
+#pragma once
+#include "EzAbilityContext.h"
+#include "Condition/EzAbilityCondition.h"
+#include "Evaluator/EzAbilityEvaluator.h"
+#include "Task/EzAbilityTask.h"
+
+//#include "EzAblityTestTypes.generated.h"
+
+class UEzAbility;
+struct FEzAbilityInstanceData;
+
+
+struct FTestEzAbilityExecutionContext : public FEzAbilityContext
+{
+	FTestEzAbilityExecutionContext(UObject& InOwner, const UEzAbility& InEzAbility, FEzAbilityInstanceData& InInstanceData)
+		: FEzAbilityContext(InOwner, InEzAbility, InInstanceData)
+	{
+	}
+	
+	struct FLogItem
+	{
+		FLogItem() = default;
+		FLogItem(const FName& InName, const FString& InMessage) : Name(InName), Message(InMessage) {}
+		FName Name;
+		FString Message; 
+	};
+	TArray<FLogItem> LogItems;
+	
+	void Log(const FName& Name, const FString& Message)
+	{
+		LogItems.Emplace(Name, Message);
+	}
+
+	void LogClear()
+	{
+		LogItems.Empty();
+	}
+
+	struct FLogOrder
+	{
+		FLogOrder(const FTestEzAbilityExecutionContext& InContext, const int32 InIndex) : Context(InContext), Index(InIndex) {}
+
+		FLogOrder Then(const FName& Name, const FString& Message) const
+		{
+			int32 NextIndex = Index;
+			while (NextIndex < Context.LogItems.Num())
+			{
+				const FLogItem& Item = Context.LogItems[NextIndex];
+				if (Item.Name == Name && Item.Message == Message)
+				{
+					break;
+				}
+				NextIndex++;
+			}
+			return FLogOrder(Context, NextIndex);
+		}
+
+		operator bool() const { return Index < Context.LogItems.Num(); }
+		
+		const FTestEzAbilityExecutionContext& Context;
+		int32 Index = 0;
+	};
+
+	FLogOrder Expect(const FName& Name, const FString& Message) const
+	{
+		return FLogOrder(*this, 0).Then(Name, Message);
+	}
+
+	template <class ...Args>
+	bool ExpectInActiveStates(const Args&... States)
+	{
+		FName ExpectedStateNames[] = { States... };
+		const int32 NumExpectedStateNames = sizeof...(States);
+
+		TArray<FName> ActiveStateNames = GetActiveStateNames();
+
+		if (ActiveStateNames.Num() != NumExpectedStateNames)
+		{
+			return false;
+		}
+
+		for (int32 Index = 0; Index != NumExpectedStateNames; Index++)
+		{
+			if (ExpectedStateNames[Index] != ActiveStateNames[Index])
+			{
+				return false;
+			}
+		}
+
+		return true;
+	}
+	
+};
+
+// USTRUCT()
+// struct FTestEval_AInstanceData
+// {
+// 	GENERATED_BODY()
+// 	
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	float FloatA = 0.0f;
+//
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	int32 IntA = 0;
+//
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	bool bBoolA = false;
+// };
+//
+// USTRUCT()
+// struct FTestEval_A : public FEzAbilityEvaluator
+// {
+// 	GENERATED_BODY()
+//
+// 	using FInstanceDataType = FTestEval_AInstanceData;
+//
+// 	FTestEval_A() = default;
+// 	FTestEval_A(const FName InName) { Name = InName; }
+// 	virtual ~FTestEval_A() override {}
+//
+// 	virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
+// };
+//
+// USTRUCT()
+// struct FTestTask_BInstanceData
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	float FloatB = 0.0f;
+//
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	int32 IntB = 0;
+//
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	bool bBoolB = false;
+// };
+//
+// USTRUCT()
+// struct FTestTask_B : public FEzAbilityTask
+// {
+// 	GENERATED_BODY()
+//
+// 	using FInstanceDataType = FTestTask_BInstanceData;
+//
+// 	FTestTask_B() = default;
+// 	FTestTask_B(const FName InName) { Name = InName; }
+// 	virtual ~FTestTask_B() override {}
+// 	
+// 	virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
+//
+// 	virtual EAbilityRunStatus EnterState(FEzAbilityContext& Context, const FEzAbilityTransitionResult& Transition) const override
+// 	{
+// 		FTestEzAbilityExecutionContext& TestContext = static_cast<FTestEzAbilityExecutionContext&>(Context);
+// 		TestContext.Log(Name,  TEXT("EnterState"));
+// 		return EAbilityRunStatus::Running;
+// 	}
+// };
+//
+// USTRUCT()
+// struct FTestTask_PrintValueInstanceData
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY(EditAnywhere, Category = "Parameter")
+// 	int32 Value = 0;
+// };
+//
+// USTRUCT()
+// struct FTestTask_PrintValue : public FEzAbilityTask
+// {
+// 	GENERATED_BODY()
+//
+// 	using FInstanceDataType = FTestTask_PrintValueInstanceData;
+//
+// 	FTestTask_PrintValue() = default;
+// 	FTestTask_PrintValue(const FName InName) { Name = InName; }
+// 	virtual ~FTestTask_PrintValue() override {}
+// 	
+// 	virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
+//
+// 	virtual EAbilityRunStatus EnterState(FEzAbilityContext& Context, const FEzAbilityTransitionResult& Transition) const override
+// 	{
+// 		FTestEzAbilityExecutionContext& TestContext = static_cast<FTestEzAbilityExecutionContext&>(Context);
+// 		const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
+// 		TestContext.Log(Name,  FString::Printf(TEXT("EnterState%d"), InstanceData.Value));
+// 		return EAbilityRunStatus::Running;
+// 	}
+//
+// 	virtual void ExitState(FEzAbilityContext& Context, const FEzAbilityTransitionResult& Transition) const override
+// 	{
+// 		FTestEzAbilityExecutionContext& TestContext = static_cast<FTestEzAbilityExecutionContext&>(Context);
+// 		const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
+// 		TestContext.Log(Name,  FString::Printf(TEXT("ExitState%d"), InstanceData.Value));
+// 	}
+//
+// 	virtual EAbilityRunStatus Tick(FEzAbilityContext& Context, const float DeltaTime) const override
+// 	{
+// 		FTestEzAbilityExecutionContext& TestContext = static_cast<FTestEzAbilityExecutionContext&>(Context);
+// 		const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
+// 		TestContext.Log(Name,  FString::Printf(TEXT("Tick%d"), InstanceData.Value));
+// 		
+// 		return EAbilityRunStatus::Running;
+// 	};
+// };
+//
+//
+// USTRUCT()
+// struct FTestTask_StopTreeInstanceData
+// {
+// 	GENERATED_BODY()
+// };
+//
+// USTRUCT()
+// struct FTestTask_StopTree : public FEzAbilityTask
+// {
+// 	GENERATED_BODY()
+//
+// 	using FInstanceDataType = FTestTask_PrintValueInstanceData;
+//
+// 	FTestTask_StopTree() = default;
+// 	explicit FTestTask_StopTree(const FName InName) { Name = InName; }
+// 	virtual ~FTestTask_StopTree() override {}
+// 	
+// 	virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
+//
+// 	virtual EAbilityRunStatus EnterState(FEzAbilityContext& Context, const FEzAbilityTransitionResult& Transition) const override
+// 	{
+// 		if (Phase == EEzAbilityUpdatePhase::EnterStates)
+// 		{
+// 			return Context.Stop();
+// 		}
+// 		return EAbilityRunStatus::Running;
+// 	}
+//
+// 	virtual void ExitState(FEzAbilityContext& Context, const FEzAbilityTransitionResult& Transition) const override
+// 	{
+// 		if (Phase == EEzAbilityUpdatePhase::ExitStates)
+// 		{
+// 			Context.Stop();
+// 		}
+// 	}
+//
+// 	virtual EAbilityRunStatus Tick(FEzAbilityContext& Context, const float DeltaTime) const override
+// 	{
+// 		if (Phase == EEzAbilityUpdatePhase::TickEzAbility)
+// 		{
+// 			return Context.Stop();
+// 		}
+// 		return EAbilityRunStatus::Running;
+// 	};
+//
+// 	/** Indicates in which phase the call to Stop should be performed. Possible values are EnterStates, ExitStats and TickEzAbility */
+// 	UPROPERTY()
+// 	EEzAbilityUpdatePhase Phase = EEzAbilityUpdatePhase::Unset;
+// };
+//
+// USTRUCT()
+// struct FTestTask_StandInstanceData
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY()
+// 	int32 CurrentTick = 0;
+// };
+//
+// USTRUCT()
+// struct FTestTask_Stand : public FEzAbilityTask
+// {
+// 	GENERATED_BODY()
+//
+// 	using FInstanceDataType = FTestTask_StandInstanceData;
+// 	
+// 	FTestTask_Stand() = default;
+// 	FTestTask_Stand(const FName InName) { Name = InName; }
+// 	virtual ~FTestTask_Stand() {}
+//
+// 	virtual const UStruct* GetInstanceDataType() const { return FInstanceDataType::StaticStruct(); }
+//
+// 	virtual EAbilityRunStatus EnterState(FEzAbilityContext& Context, const FEzAbilityTransitionResult& Transition) const override
+// 	{
+// 		FTestEzAbilityExecutionContext& TestContext = static_cast<FTestEzAbilityExecutionContext&>(Context);
+// 		TestContext.Log(Name, TEXT("EnterState"));
+//
+// 		FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
+// 		
+// 		if (Transition.ChangeType == EEzAbilityStateChangeType::Changed)
+// 		{
+// 			InstanceData.CurrentTick = 0;
+// 		}
+// 		return EnterStateResult;
+// 	}
+//
+// 	virtual void ExitState(FEzAbilityContext& Context, const FEzAbilityTransitionResult& Transition) const override
+// 	{
+// 		FTestEzAbilityExecutionContext& TestContext = static_cast<FTestEzAbilityExecutionContext&>(Context);
+//
+// 		if (Transition.CurrentRunStatus == EAbilityRunStatus::Succeeded)
+// 		{
+// 			TestContext.Log(Name, TEXT("ExitSucceeded"));
+// 		}
+// 		else if (Transition.CurrentRunStatus == EAbilityRunStatus::Failed)
+// 		{
+// 			TestContext.Log(Name, TEXT("ExitFailed"));
+// 		}
+// 		else if (Transition.CurrentRunStatus == EAbilityRunStatus::Stopped)
+// 		{
+// 			TestContext.Log(Name, TEXT("ExitStopped"));
+// 		}
+// 		
+// 		TestContext.Log(Name, TEXT("ExitState"));
+// 	}
+//
+// 	virtual void StateCompleted(FEzAbilityContext& Context, const EAbilityRunStatus CompletionStatus, const FEzAbilityActiveStates& CompletedActiveStates) const override
+// 	{
+// 		FTestEzAbilityExecutionContext& TestContext = static_cast<FTestEzAbilityExecutionContext&>(Context);
+// 		TestContext.Log(Name, TEXT("StateCompleted"));
+// 	}
+// 	
+// 	virtual EAbilityRunStatus Tick(FEzAbilityContext& Context, const float DeltaTime) const override
+// 	{
+// 		FTestEzAbilityExecutionContext& TestContext = static_cast<FTestEzAbilityExecutionContext&>(Context);
+// 		TestContext.Log(Name, TEXT("Tick"));
+//
+// 		FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
+// 		
+// 		InstanceData.CurrentTick++;
+// 		
+// 		return (InstanceData.CurrentTick >= TicksToCompletion) ? TickCompletionResult : EAbilityRunStatus::Running;
+// 	};
+//
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	int32 TicksToCompletion = 1;
+//
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	EAbilityRunStatus TickCompletionResult = EAbilityRunStatus::Succeeded;
+//
+// 	UPROPERTY(EditAnywhere, Category = Parameter)
+// 	EAbilityRunStatus EnterStateResult = EAbilityRunStatus::Running;
+// };
+//
+// USTRUCT()
+// struct FEzAbilityTestConditionInstanceData
+// {
+// 	GENERATED_BODY()
+// 	
+// 	UPROPERTY(EditAnywhere, Category = Parameters)
+// 	int32 Count = 1;
+//
+// 	static std::atomic<int32> GlobalCounter;
+// };
+//
+// USTRUCT(meta = (Hidden))
+// struct FEzAbilityTestCondition : public FEzAbilityCondition
+// {
+// 	GENERATED_BODY()
+//
+// 	using FInstanceDataType = FEzAbilityTestConditionInstanceData;
+//
+// 	FEzAbilityTestCondition() = default;
+//
+// 	virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
+// 	virtual bool TestCondition(FEzAbilityContext& Context) const override
+// 	{
+// 		FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
+// 		InstanceData.GlobalCounter.fetch_add(InstanceData.Count);
+// 		return true;
+// 	}
+// };
+//
+// struct FEzAbilityTestRunContext
+// {
+// 	int32 Count = 0;
+// };
+//
+//
+// USTRUCT()
+// struct FEzAbilityTest_PropertyStructB
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	int32 B = 0;
+// };
+//
+// USTRUCT()
+// struct FEzAbilityTest_PropertyStruct
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	int32 A = 0;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	int32 B = 0;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	FEzAbilityTest_PropertyStructB StructB;
+// };
+//
+// UCLASS(HideDropdown)
+// class UEzAbilityTest_PropertyObjectInstanced : public UObject
+// {
+// 	GENERATED_BODY()
+// public:
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	int32 A = 0;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	FInstancedStruct InstancedStruct;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TArray<FGameplayTag> ArrayOfTags;
+// };
+//
+// UCLASS(HideDropdown)
+// class UEzAbilityTest_PropertyObjectInstancedWithB : public UEzAbilityTest_PropertyObjectInstanced
+// {
+// 	GENERATED_BODY()
+// public:
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	int32 B = 0;
+// };
+//
+// UCLASS(HideDropdown)
+// class UEzAbilityTest_PropertyObject : public UObject
+// {
+// 	GENERATED_BODY()
+// public:
+// 	
+// 	UPROPERTY(EditAnywhere, Instanced, Category = "")
+// 	TObjectPtr<UEzAbilityTest_PropertyObjectInstanced> InstancedObject;
+//
+// 	UPROPERTY(EditAnywhere, Instanced, Category = "")
+// 	TArray<TObjectPtr<UEzAbilityTest_PropertyObjectInstanced>> ArrayOfInstancedObjects;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TArray<int32> ArrayOfInts;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	FInstancedStruct InstancedStruct;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TArray<FInstancedStruct> ArrayOfInstancedStructs;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	FEzAbilityTest_PropertyStruct Struct;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TArray<FEzAbilityTest_PropertyStruct> ArrayOfStruct;
+// };
+//
+// UCLASS(HideDropdown)
+// class UEzAbilityTest_PropertyObject2 : public UObject
+// {
+// 	GENERATED_BODY()
+// public:
+// };
+//
+// USTRUCT()
+// struct FEzAbilityTest_PropertyCopy
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	FEzAbilityTest_PropertyStruct Item;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TArray<FEzAbilityTest_PropertyStruct> Array;
+// };
+//
+// USTRUCT()
+// struct FEzAbilityTest_PropertyRefSourceStruct
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	FEzAbilityTest_PropertyStruct Item;
+//
+// 	UPROPERTY(EditAnywhere, Category = "Output")
+// 	FEzAbilityTest_PropertyStruct OutputItem;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TArray<FEzAbilityTest_PropertyStruct> Array;
+// };
+//
+// USTRUCT()
+// struct FEzAbilityTest_PropertyRefTargetStruct
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY(EditAnywhere, Category = "", meta = (RefType = "/Script/EzAbilityTestSuite.EzAbilityTest_PropertyStruct"))
+// 	FEzAbilityPropertyRef RefToStruct;
+//
+// 	UPROPERTY(EditAnywhere, Category = "", meta = (RefType = "Int32"))
+// 	FEzAbilityPropertyRef RefToInt;
+//
+// 	UPROPERTY(EditAnywhere, Category = "", meta = (RefType = "/Script/EzAbilityTestSuite.EzAbilityTest_PropertyStruct", IsRefToArray))
+// 	FEzAbilityPropertyRef RefToStructArray;
+// };
+//
+// USTRUCT()
+// struct FEzAbilityTest_PropertyCopyObjects
+// {
+// 	GENERATED_BODY()
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TObjectPtr<UObject> Object;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TSubclassOf<UObject> Class;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TSoftObjectPtr<UObject> SoftObject;
+//
+// 	UPROPERTY(EditAnywhere, Category = "")
+// 	TSoftClassPtr<UObject> SoftClass;
+// };