PropertyBags - No way to read/access Set containers?

We have been writing some code trying to use Property Bags and have ran into a bit of an issue with set containers on them. As mentioned in the repro steps- FPropertyBagSetRef have no public functions for iterating or reading values in the set, making it impossible to read values in a set in a property bag (or even empty it since it privately inherits from FScriptSetHelper). The only publicly available functions on the type are Add(s), Remove, Contains, and Num.

Is this intentional or has FPropertyBagSetRef not been fully implemented? Or am I missing something and we are supposed to read from these sets some other way? Any help (or code fixes) would be appreciated.

Thanks!

Edit: I’ve just worked around this for now by using an array and converting from/to a set as needed.

Steps to Reproduce

  1. Create an FInstancedPropertyBag in code
  2. Add a Set property too it - e.g.
    1. Bag.AddProperties({{ TEXT(“SetKey”), EPropertyBagContainerType::Set, EPropertyBagPropertyType::Float }});
  3. Attempt to figure out how to read values out of the set (or empty it)
  4. Sad

Hi,

You can check \Engine\Source\Developer\StructUtilsTestSuite\Private\PropertyBagTest.cpp for several test. I asked AI to add one showing how to iterate and print value of those properties. I ran it in the Editor successfully, here the test you can add to the file (AI inserted it on line 697):

struct FTest_IterateAndPrintValues : FAITestBase
{
	virtual bool InstantTest() override
	{
		static const FName BoolName(TEXT("Bool"));
		static const FName Int32Name(TEXT("Int32"));
		static const FName FloatName(TEXT("Float"));
		static const FName NameName(TEXT("Name"));
		static const FName StringName(TEXT("String"));
		static const FName EnumName(TEXT("Enum"));
		static const FName StructName(TEXT("Struct"));
		static const FName Int32ArrayName(TEXT("Int32Array"));
		static const FName FloatSetName(TEXT("FloatSet"));
 
		FInstancedPropertyBag Bag;
		Bag.AddProperties({
			{ BoolName,       EPropertyBagPropertyType::Bool },
			{ Int32Name,      EPropertyBagPropertyType::Int32 },
			{ FloatName,      EPropertyBagPropertyType::Float },
			{ NameName,       EPropertyBagPropertyType::Name },
			{ StringName,     EPropertyBagPropertyType::String },
			{ EnumName,       EPropertyBagPropertyType::Enum,   StaticEnum<EPropertyBagTest1>() },
			{ StructName,     EPropertyBagPropertyType::Struct, FTestStructHashable1::StaticStruct() },
			{ Int32ArrayName, EPropertyBagContainerType::Array, EPropertyBagPropertyType::Int32 },
			{ FloatSetName,   EPropertyBagContainerType::Set,   EPropertyBagPropertyType::Float },
		});
 
		AITEST_EQUAL(TEXT("Set bool should succeed"),   Bag.SetValueBool(BoolName, true),                            EPropertyBagResult::Success);
		AITEST_EQUAL(TEXT("Set int32 should succeed"),  Bag.SetValueInt32(Int32Name, 42),                            EPropertyBagResult::Success);
		AITEST_EQUAL(TEXT("Set float should succeed"),  Bag.SetValueFloat(FloatName, 3.14f),                         EPropertyBagResult::Success);
		AITEST_EQUAL(TEXT("Set name should succeed"),   Bag.SetValueName(NameName, FName(TEXT("Hello"))),            EPropertyBagResult::Success);
		AITEST_EQUAL(TEXT("Set string should succeed"), Bag.SetValueString(StringName, TEXT("World")),               EPropertyBagResult::Success);
		AITEST_EQUAL(TEXT("Set enum should succeed"),   Bag.SetValueEnum(EnumName, EPropertyBagTest1::Bar),          EPropertyBagResult::Success);
 
		FTestStructHashable1 StructValue;
		StructValue.Float = 7.0f;
		AITEST_EQUAL(TEXT("Set struct should succeed"), Bag.SetValueStruct(StructName, FConstStructView::Make(StructValue)), EPropertyBagResult::Success);
 
		if (TValueOrError<FPropertyBagArrayRef, EPropertyBagResult> ArrayRes = Bag.GetMutableArrayRef(Int32ArrayName); ArrayRes.IsValid())
		{
			FPropertyBagArrayRef Array = ArrayRes.GetValue();
			for (const int32 Value : { 10, 20, 30 })
			{
				const int32 Index = Array.AddValue();
				Array.SetValueInt32(Index, Value);
			}
		}
 
		if (TValueOrError<FPropertyBagSetRef, EPropertyBagResult> SetRes = Bag.GetMutableSetRef(FloatSetName); SetRes.IsValid())
		{
			FPropertyBagSetRef Set = SetRes.GetValue();
			Set.AddValueFloat(1.5f);
			Set.AddValueFloat(2.5f);
			Set.AddValueFloat(3.5f);
		}
 
		const UPropertyBag* BagStruct = Bag.GetPropertyBagStruct();
		AITEST_TRUE(TEXT("Bag struct should be valid"), BagStruct != nullptr);
 
		int32 NumPropsSeen = 0;
		for (const FPropertyBagPropertyDesc& Desc : BagStruct->GetPropertyDescs())
		{
			++NumPropsSeen;
 
			const EPropertyBagContainerType ContainerType = Desc.ContainerTypes.GetFirstContainerType();
 
			if (ContainerType == EPropertyBagContainerType::Array)
			{
				const TValueOrError<const FPropertyBagArrayRef, EPropertyBagResult> ArrayRes = Bag.GetArrayRef(Desc);
				AITEST_TRUE(TEXT("Array ref should be valid"), ArrayRes.IsValid());
 
				const FPropertyBagArrayRef& Array = ArrayRes.GetValue();
				UE_LOG(LogTemp, Display, TEXT("Array  %s [%d elem(s)]"), *Desc.Name.ToString(), Array.Num());
				for (int32 Index = 0; Index < Array.Num(); ++Index)
				{
					if (Desc.ValueType == EPropertyBagPropertyType::Int32)
					{
						UE_LOG(LogTemp, Display, TEXT("         [%d] = %d"), Index, Array.GetValueInt32(Index).GetValue());
					}
				}
			}
			else if (ContainerType == EPropertyBagContainerType::Set)
			{
				// FPropertyBagSetRef does not expose element iteration in 5.7, so drop down to FScriptSetHelper.
				const FSetProperty* SetProperty = CastField<FSetProperty>(Desc.CachedProperty);
				AITEST_TRUE(TEXT("Set property should be valid"), SetProperty != nullptr);
 
				const void* SetAddress = SetProperty->ContainerPtrToValuePtr<void>(Bag.GetValue().GetMemory());
				FScriptSetHelper SetHelper(SetProperty, SetAddress);
 
				UE_LOG(LogTemp, Display, TEXT("Set    %s [%d elem(s)]"), *Desc.Name.ToString(), SetHelper.Num());
				for (int32 Index = 0; Index < SetHelper.GetMaxIndex(); ++Index)
				{
					if (!SetHelper.IsValidIndex(Index))
					{
						continue;
					}
					const void* ElementPtr = SetHelper.GetElementPtr(Index);
					if (Desc.ValueType == EPropertyBagPropertyType::Float)
					{
						UE_LOG(LogTemp, Display, TEXT("         %f"), *static_cast<const float*>(ElementPtr));
					}
				}
			}
			else
			{
				switch (Desc.ValueType)
				{
				case EPropertyBagPropertyType::Bool:
					UE_LOG(LogTemp, Display, TEXT("Bool   %s = %s"),
						*Desc.Name.ToString(), Bag.GetValueBool(Desc).GetValue() ? TEXT("true") : TEXT("false"));
					break;
				case EPropertyBagPropertyType::Int32:
					UE_LOG(LogTemp, Display, TEXT("Int32  %s = %d"),
						*Desc.Name.ToString(), Bag.GetValueInt32(Desc).GetValue());
					break;
				case EPropertyBagPropertyType::Float:
					UE_LOG(LogTemp, Display, TEXT("Float  %s = %f"),
						*Desc.Name.ToString(), Bag.GetValueFloat(Desc).GetValue());
					break;
				case EPropertyBagPropertyType::Name:
					UE_LOG(LogTemp, Display, TEXT("Name   %s = %s"),
						*Desc.Name.ToString(), *Bag.GetValueName(Desc).GetValue().ToString());
					break;
				case EPropertyBagPropertyType::String:
					UE_LOG(LogTemp, Display, TEXT("String %s = %s"),
						*Desc.Name.ToString(), *Bag.GetValueString(Desc).GetValue());
					break;
				case EPropertyBagPropertyType::Enum:
					{
						const UEnum* EnumType = Cast<const UEnum>(Desc.ValueTypeObject);
						const TValueOrError<uint8, EPropertyBagResult> EnumRes = Bag.GetValueEnum(Desc, EnumType);
						UE_LOG(LogTemp, Display, TEXT("Enum   %s = %s (%d)"),
							*Desc.Name.ToString(),
							EnumType ? *EnumType->GetNameStringByValue(EnumRes.GetValue()) : TEXT("<unknown>"),
							EnumRes.GetValue());
					}
					break;
				case EPropertyBagPropertyType::Struct:
					{
						const TValueOrError<FStructView, EPropertyBagResult> StructRes = Bag.GetValueStruct(Desc);
						if (StructRes.IsValid())
						{
							if (const FTestStructHashable1* TypedPtr = StructRes.GetValue().GetPtr<const FTestStructHashable1>())
							{
								UE_LOG(LogTemp, Display, TEXT("Struct %s = (FTestStructHashable1 Float=%f)"),
									*Desc.Name.ToString(), TypedPtr->Float);
							}
						}
					}
					break;
				default:
					UE_LOG(LogTemp, Display, TEXT("Other  %s = <type %d not printed>"),
						*Desc.Name.ToString(), static_cast<int32>(Desc.ValueType));
					break;
				}
			}
		}
 
		AITEST_EQUAL(TEXT("Iteration should visit every property"), NumPropsSeen, 9);
 
		return true;
	}
};
IMPLEMENT_AI_INSTANT_TEST(FTest_IterateAndPrintValues, "System.StructUtils.PropertyBag.IterateAndPrintValues");

Regards,

Patrick

That looks pretty gnarly as you have to manually make a FScriptSetHelper in order to read the set instead of using the FPropertyBagSetRef (which itself is an FScriptSetHelper, and the array version uses the FPropertyBagArrayRef without the need for an extra FScriptArrayHelper) and you have to construct it by picking the property type off the set property description and getting the bag value memory? (Like, the AI generated code event calls out ‘// FPropertyBagSetRef does not expose element iteration in 5.7, so drop down to FScriptSetHelper.’). This also loses all the TValueOrError functionality/nice getters other bag values use?

I agree this code works but this isn’t what I would call a proper API for working with sets within property bags (especially when the Array type has proper support). I will stick to storing and reading the set back as an array for now and would encourage improving this but if Epic feels what is there is all that’s needed I wouldn’t try to fight for this.