I’ve come up with a potential solution for this issue, and it seems to be working for my company (at least in our use-cases), so I thought I’d share it here. That said, State Trees are a pretty massive system, so it’s possible I’ve missed some cases or am going against some assumptions the State Tree engineers made for this system.
In FStateTreeExecutionContext, I added the function:
bool FStateTreeExecutionContext::IsContextDataCompatible(const UStateTree* OtherStateTree) const
{
if (OtherStateTree == nullptr || OtherStateTree->GetSchema() == nullptr || RootStateTree.GetSchema() == nullptr)
{
return false;
}
// Verify that the OtherStateTree context actor/controller class are children of RootStateTree's contexts
if (!RootStateTree.HasCompatibleContextData(*OtherStateTree))
{
return false;
}
const TConstArrayView<FStateTreeExternalDataDesc>& RootContextDescs = RootStateTree.ContextDataDescs;
const TConstArrayView<FStateTreeExternalDataDesc>& ItemContextDescs = OtherStateTree->ContextDataDescs;
if (RootContextDescs.Num() != ItemContextDescs.Num())
{
return false;
}
const int32 Num = RootContextDescs.Num();
for (int32 Index = 0; Index < Num; Index++)
{
const FStateTreeExternalDataDesc& OtherDataDesc = ItemContextDescs[Index];
const UStruct* OwnerData = ContextAndExternalDataViews[Index].GetStruct();
// Verify that the Owner is compatible with the context data for the State Tree we're trying to override
if (!OwnerData || !OwnerData ->IsChildOf(OtherDataDesc.Struct))
{
return false;
}
}
return true;
}
Then, in FStateTreeExecutionContext::SetLinkedStateTreeOverrides, we want to make a modification to the code:
void FStateTreeExecutionContext::SetLinkedStateTreeOverrides(FStateTreeReferenceOverrides InLinkedStateTreeOverrides)
{
// Leave everything the same until we get to the * * * comments
bool bValid = true;
// Confirms that the overrides schema matches.
const TConstArrayView<FStateTreeReferenceOverrideItem> InOverrideItems = InLinkedStateTreeOverrides.GetOverrideItems();
for (const FStateTreeReferenceOverrideItem& Item : InOverrideItems)
{
if (const UStateTree* ItemStateTree = Item.GetStateTreeReference().GetStateTree())
{
if (!ItemStateTree->IsReadyToRun())
{
STATETREE_LOG(Error, TEXT("%hs: '%s' using StateTree '%s' trying to set override '%s' but the tree is not initialized properly."),
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(GetStateTree()), *GetFullNameSafe(ItemStateTree));
bValid = false;
break;
}
// * * * This is where the modification starts; the rest is there for reference * * * //
if (!ItemStateTree->HasCompatibleContextData(RootStateTree)) // This was originally backwards, which I made another post about
{
// Allow compatibility if OverrideTree.ContextActor is a child of RootStateTree.ContextActor
if (!IsContextDataCompatible(ItemStateTree))
{
STATETREE_LOG(Error, TEXT("%hs: '%s' using StateTree '%s' trying to set override '%s' but the tree context data is not compatible."),
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(GetStateTree()), *GetFullNameSafe(ItemStateTree));
bValid = false;
break;
}
}
// * * * This is where the modification end; the rest is there for reference * * * //
// Leave the rest of the code the same
}
}
}
And then we want to do the same in FStateTreeExecutionContext::SelectStateInternal:
bool FStateTreeExecutionContext::SelectStateInternal(
const FStateTreeExecutionFrame* CurrentParentFrame,
FStateTreeExecutionFrame& CurrentFrame,
const FStateTreeExecutionFrame* CurrentFrameInActiveFrames,
TConstArrayView<FStateTreeStateHandle> PathToNextState,
FStateSelectionResult& OutSelectionResult,
const FStateTreeSharedEvent* TransitionEvent)
{
// Leave everything the same until we get to the * * * comments
else if (NextState.Type == EStateTreeStateType::LinkedAsset)
{
if (NextLinkedStateAsset == nullptr || NextLinkedStateAsset->States.Num() == 0)
{
break;
}
if (OutSelectionResult.IsFull())
{
STATETREE_LOG(Error, TEXT("%hs: Reached max execution depth when trying to select state %s from '%s'. '%s' using StateTree '%s'."),
__FUNCTION__, *GetSafeStateName(CurrentFrame, NextStateHandle), *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(CurrentFrame.StateTree));
break;
}
// The linked state tree should have compatible context requirements.
if (!NextLinkedStateAsset->HasCompatibleContextData(RootStateTree)
|| NextLinkedStateAsset->GetSchema()->GetClass() != RootStateTree.GetSchema()->GetClass())
{
// * * * This is where the modification starts; the rest is there for reference * * * //
if (!IsContextDataCompatible(NextLinkedStateAsset))
{
STATETREE_LOG(Error, TEXT("%hs: The linked State Tree '%s' does not have compatible schema, trying to select state %s from '%s'. '%s' using StateTree '%s'."),
__FUNCTION__, *GetFullNameSafe(NextLinkedStateAsset), *GetSafeStateName(CurrentFrame, NextStateHandle), *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(CurrentFrame.StateTree));
break;
}
// * * * This is where the modification ends; the rest is there for reference * * * //
}
// Leave everything else the same
}
}
There is one case I’ve noticed where this doesn’t work: if the State Tree is not running (i.e. bStartAutomatically is false on the StateTreeComponent and you call AddLinkedStateOverrides before starting the tree), then the Owner’s context data gets reset. This appears to be due to the order-of-operations in UStateTreeComponent::SetContextRequirements. To fix this, I just swapped the order a bit:
bool UStateTreeComponent::SetContextRequirements(FStateTreeExecutionContext& Context, bool bLogErrors)
{
Context.SetCollectExternalDataCallback(FOnCollectStateTreeExternalData::CreateUObject(this, &UStateTreeComponent::CollectExternalData));
const bool bSuccess = UStateTreeComponentSchema::SetContextRequirements(*this, Context);
Context.SetLinkedStateTreeOverrides(LinkedStateTreeOverrides);
return bSuccess;
}
As stated before, this seems to work well for our cases in initial testing, but there are likely a lot of cases that are untested. Just thought I’d share in case others found it helpful as well.