Thanks for all the information.
Here’s my FAttributeSetInitterCurveEval struct i came up with, if anyone is still interested.
Works with 4.25.
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemGlobals.h"
#include <AttributeSet.h>
#include <AbilitySystemComponent.h>
// thanks to : https://forums.unrealengine.com/community/community-content-tools-and-tutorials/116578-comprehensive-gameplayabilities-analysis-series
struct RPG_API FAttributeSetInitterCurveEval : public FAttributeSetInitter
{
struct FPropertyCurvePair
{
FPropertyCurvePair(FProperty* InProperty, FRealCurve* InCurve)
: Property(InProperty), Curve(InCurve)
{
}
FProperty* Property;
FRealCurve* Curve;
};
struct FAttributeDefaultCurveList
{
struct FPropertyCurvePair
{
FPropertyCurvePair(FProperty* InProperty, FRealCurve* InCurve)
: Property(InProperty), Curve(InCurve)
{
}
FProperty* Property;
FRealCurve* Curve;
};
void AddPair(FProperty* InProperty, FRealCurve* InValue)
{
List.Add(FPropertyCurvePair(InProperty, InValue));
}
TArray<FPropertyCurvePair> List;
};
struct FAttributeSetDefaultsCurveCollection
{
TMap<TSubclassOf<UAttributeSet>, FAttributeDefaultCurveList> DataMap;
};
TMap<FName, FAttributeSetDefaultsCurveCollection> Defaults;
bool IsSupportedProperty(FProperty* Property) const
{
return (Property && (CastField<FNumericProperty>(Property) || FGameplayAttribute::IsGameplayAttributeDataProperty(Property)));
}
TSubclassOf<UAttributeSet> RPGFindBestAttributeClass(TArray<TSubclassOf<UAttributeSet> >& ClassList, FString PartialName)
{
for (auto Class : ClassList)
{
if (Class->GetName().Contains(PartialName))
{
return Class;
}
}
return nullptr;
}
virtual void PreloadAttributeSetData(const TArray<UCurveTable*>& CurveData) override
{
if (!ensure(CurveData.Num() > 0))
{
return;
}
/**
* Get list of AttributeSet classes loaded
*/
TArray<TSubclassOf<UAttributeSet> > ClassList;
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* TestClass = *ClassIt;
if (TestClass->IsChildOf(UAttributeSet::StaticClass()))
{
ClassList.Add(TestClass);
/*#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
// This can only work right now on POD attribute sets. If we ever support FStrings or TArrays in AttributeSets
// we will need to update this code to not use memcpy etc.
for (TFieldIterator<UProperty> PropIt(TestClass, EFieldIteratorFlags::IncludeSuper); PropIt; ++PropIt)
{
if (!PropIt->HasAllPropertyFlags(CPF_IsPlainOldData))
{
ABILITY_LOG(Error, TEXT("FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData Unable to Handle AttributeClass %s because it has a non POD property: %s"),
*TestClass->GetName(), *PropIt->GetName());
return;
}
}
#endif*/
}
}
//Iterate over the table array...
for (const UCurveTable* CurTable : CurveData)
{
for (const TPair<FName, FRealCurve*>& CurveRow : CurTable->GetRowMap())
{
//The entire row name, i.e. Class.Player.MaxHealth
FString RowName = CurveRow.Key.ToString();
FString ClassName;
FString SetName;
FString AttributeName;
FString Temp;
//Split the RowName into ClassName (Class) and the put the rest in Temp (Player.MaxHealth)
RowName.Split(TEXT("."), &ClassName, &Temp);
//Split the remainder into the SetName (Player) and the AttributeName (MaxHealth)
Temp.Split(TEXT("."), &SetName, &AttributeName);
//If some of these ended up unpopulated just disregard this row...
if (!ensure(!ClassName.IsEmpty() && !SetName.IsEmpty() && !AttributeName.IsEmpty()))
{
ABILITY_LOG(Warning, TEXT("FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData Unable to parse row %s in %s"), *RowName, *CurTable->GetName());
continue;
}
// Find the AttributeSet
TSubclassOf<UAttributeSet> Set = RPGFindBestAttributeClass(ClassList, SetName);
if (!Set)
{
// This is ok, we may have rows in here that don't correspond directly to attributes
ABILITY_LOG(Verbose, TEXT("FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData Unable to match AttributeSet from %s (row: %s)"), *SetName, *RowName);
continue;
}
// Find the UProperty
FProperty* Property = FindFProperty<FProperty>(*Set, *AttributeName);
//The IsSupportedProperty() just does: return (Property && (Cast<UNumericProperty>(Property) || FGameplayAttribute::IsGameplayAttributeDataProperty(Property)));
//meaning "is this a number of a FGameplayAttribute?"
if (!IsSupportedProperty(Property))
{
ABILITY_LOG(Verbose, TEXT("FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData Unable to match Attribute from %s (row: %s)"), *AttributeName, *RowName);
continue;
}
FRealCurve* Curve = CurveRow.Value;
FName ClassFName = FName(*ClassName);
//Get the rich curve collection corresponding to our ClassName (or create it)
FAttributeSetDefaultsCurveCollection& DefaultCollection = Defaults.FindOrAdd(ClassFName);
int32 LastLevel = Curve->GetKeyTime(Curve->GetLastKeyHandle());
//DefaultCollection.DataMap.SetNum(FMath::Max(LastLevel, DefaultCollection.DataMap.Num()));
//At this point we know the Name of this "class"/"group", the AttributeSet, and the Property Name. Now loop through the values on the curve to get the attribute default value at each level.
for (auto KeyIter = Curve->GetKeyHandleIterator(); KeyIter; ++KeyIter)
{
const FKeyHandle& KeyHandle = *KeyIter;
TPair<float, float> LevelValuePair = Curve->GetKeyTimeValuePair(KeyHandle);
int32 Level = LevelValuePair.Key;
float Value = LevelValuePair.Value;
//Find the attribute list matching the current UAttributeSet
FAttributeDefaultCurveList* DefaultDataList = DefaultCollection.DataMap.Find(Set);
if (DefaultDataList == nullptr)
{
//If there is no list matching this attribute set... create it.
ABILITY_LOG(Verbose, TEXT("Initializing new default set for %s. PropertySize: %d.. DefaultSize: %d"), *Set->GetName(), Set->GetPropertiesSize(), UAttributeSet::StaticClass()->GetPropertiesSize());
DefaultDataList = &DefaultCollection.DataMap.Add(Set);
}
// Import curve value into default data
//Just add the current property together with its matching curve to the attribute list.
check(DefaultDataList);
DefaultDataList->AddPair(Property, Curve);
}
}
}
}
virtual void InitAttributeSetDefaults(UAbilitySystemComponent* AbilitySystemComponent, FName GroupName, int32 Level, bool bInitialInit) const override
{
//The profiler counter is commented out here, even though it exists in the Discrete Levels version. It seems that, despite these stats existing in AbilitySystemStats.h,
//they're not exported out of the module so they can't be used in your game module. You'll have to make your own stats if you want to profile this. I haven't gotten around to doing that yet.
//SCOPE_CYCLE_COUNTER(STAT_InitAttributeSetDefaults);
check(AbilitySystemComponent != nullptr);
//This whole block will look if the provided group exists in the preloaded data. If it doesn't it checks for the Default group. If that isn't there either, the whole operation is stopped.
const FAttributeSetDefaultsCurveCollection* Collection = Defaults.Find(GroupName);
if (!Collection)
{
ABILITY_LOG(Warning, TEXT("Unable to find DefaultAttributeSet Group %s. Failing back to Defaults"), *GroupName.ToString());
Collection = Defaults.Find(FName(TEXT("Default")));
if (!Collection)
{
ABILITY_LOG(Error, TEXT("FAttributeSetInitterDiscreteLevels::InitAttributeSetDefaults Default DefaultAttributeSet not found! Skipping Initialization"));
return;
}
}
//Iterate over all the spawned attribute sets of the provided ASC
for (const UAttributeSet* Set : AbilitySystemComponent->SpawnedAttributes)
{
//Check our preloaded data to see if we have any curves for the givenn attribute set...
const FAttributeDefaultCurveList* DefaultDataList = Collection->DataMap.Find(Set->GetClass());
if (DefaultDataList)
{
ABILITY_LOG(Log, TEXT("Initializing Set %s"), *Set->GetName());
//We found data for the given attribute set. Iterate over it and populate the data
for (auto& DataPair : DefaultDataList->List)
{
check(DataPair.Property);
if (Set->ShouldInitProperty(bInitialInit, DataPair.Property))
{
FGameplayAttribute AttributeToModify(DataPair.Property);
AbilitySystemComponent->SetNumericAttributeBase(AttributeToModify, DataPair.Curve->Eval(Level));
}
}
}
}
AbilitySystemComponent->ForceReplication();
}
virtual void ApplyAttributeDefault(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute& InAttribute, FName GroupName, int32 Level) const override
{
//The profiler counter is commented out here, even though it exists in the Discrete Levels version. It seems that, despite these stats existing in AbilitySystemStats.h,
//they're not exported out of the module so they can't be used in your game module. You'll have to make your own stats if you want to profile this. I haven't gotten around to doing that yet.
//SCOPE_CYCLE_COUNTER(STAT_InitAttributeSetDefaults);
check(AbilitySystemComponent != nullptr);
//This whole block will look if the provided group exists in the preloaded data. If it doesn't it checks for the Default group. If that isn't there either, the whole operation is stopped.
const FAttributeSetDefaultsCurveCollection* Collection = Defaults.Find(GroupName);
if (!Collection)
{
ABILITY_LOG(Warning, TEXT("Unable to find DefaultAttributeSet Group %s. Failing back to Defaults"), *GroupName.ToString());
Collection = Defaults.Find(FName(TEXT("Default")));
if (!Collection)
{
ABILITY_LOG(Error, TEXT("FAttributeSetInitterDiscreteLevels::InitAttributeSetDefaults Default DefaultAttributeSet not found! Skipping Initialization"));
return;
}
}
//Iterate over all the spawned attribute sets of the provided ASC
for (const UAttributeSet* Set : AbilitySystemComponent->SpawnedAttributes)
{
//Check our preloaded data to see if we have any curves for the givenn attribute set...
const FAttributeDefaultCurveList* DefaultDataList = Collection->DataMap.Find(Set->GetClass());
if (DefaultDataList)
{
ABILITY_LOG(Log, TEXT("Initializing Set %s"), *Set->GetName());
//We found data for the given attribute set. Iterate over it and populate the data
for (auto& DataPair : DefaultDataList->List)
{
check(DataPair.Property);
if (DataPair.Property == InAttribute.GetUProperty())
{
FGameplayAttribute AttributeToModify(DataPair.Property);
AbilitySystemComponent->SetNumericAttributeBase(AttributeToModify, DataPair.Curve->Eval(Level));
}
}
}
}
AbilitySystemComponent->ForceReplication();
}
};