Error with Fast TArray, "Oldmap size (x) does not match item count (y)"

I am working on a replicated inventory component using FFastArraySerializerItem and FFastArraySerializer.

I am getting a strange error in the editor with my FFastArraySerializer array.

In the editor I have this message, “LogNetFastTArray: Warning: OldMap size (1) does not match item count (2)”.

The engine source has this line in FFastArraySeralizer:

UE_LOG(LogNetFastTArray, Warning, TEXT("OldMap size (%d) does not match item count (%d) for struct (%s), missing a MarkArrayDirty on element add/remove?"), OldIDToKeyMap.Num(), ArraySerializer.CachedNumItemsToConsiderForWriting, *Struct->GetOwnerStruct()->GetName());

I have marked each item as dirty and also tried using MarkArrayDirty() after changes. Not sure what could be the cause or if I am overlooking something.

FFastArraySerializerItem and FFastArraySerializer code:

USTRUCT(BlueprintType)
struct INVENTORYSYSTEM_API FInventoryItem : public FFastArraySerializerItem
{
 
	GENERATED_USTRUCT_BODY()
 
public:
 
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "FInventoryItem")
		int32 Amount;
 
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "FInventoryItem")
		int32 MaxAmount;
 
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "FInventoryItem")
		bool bCanStack = false;
 
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "FInventoryItem")
		UInventoryItemAsset* ItemAsset = nullptr;
 
	// Item property key value pairs for dynamic properties
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Inventory System", DisplayName = "Dynamic Item Properties")
		FItemPropertyArray ItemProperties;
 
	void PreReplicatedRemove(const struct FInventoryArray& InArraySerializer);
	void PostReplicatedAdd(const struct FInventoryArray& InArraySerializer);
	void PostReplicatedChange(const struct FInventoryArray& InArraySerializer);
 
	FInventoryItem();
 
	FInventoryItem(int32 NewAmount, int32 NewMaxAmount, bool bNewCanStack, UInventoryItemAsset* NewItemAsset);
 
	friend bool operator== (const FInventoryItem& First, const FInventoryItem& Other)
	{
		return (First.ItemAsset == Other.ItemAsset && First.ItemProperties.Properties == Other.ItemProperties.Properties);
	}
 
	friend bool operator!= (const FInventoryItem& First, const FInventoryItem& Other)
	{
		return (First.ItemAsset != Other.ItemAsset || First.ItemProperties.Properties != Other.ItemProperties.Properties);
	}
 
	friend bool operator>= (const FInventoryItem& First, const FInventoryItem& Other)
	{
		return (First.Amount >= Other.Amount);
	}
 
	friend bool operator<= (const FInventoryItem& First, const FInventoryItem& Other)
	{
		return (First.Amount <= Other.Amount);
	}
 
	friend bool operator> (const FInventoryItem& First, const FInventoryItem& Other)
	{
		return (First.Amount > Other.Amount);
	}
 
	friend bool operator< (const FInventoryItem& First, const FInventoryItem& Other)
	{
		return (First.Amount < Other.Amount);
	}
};
 
 
USTRUCT(BlueprintType)
struct FInventoryArray : public FFastArraySerializer
{
	GENERATED_USTRUCT_BODY()
 
	UPROPERTY(BlueprintReadWrite, EditAnywhere)
		TArray< FInventoryItem > Items;
 
	bool NetDeltaSerialize(FNetDeltaSerializeInfo & DeltaParms);
 
	FInventoryArrayChangedDelegate InventoryArrayChangedDelegate;
};
 
    template<>
struct TStructOpsTypeTraits< FInventoryArray > : public TStructOpsTypeTraitsBase2< FInventoryArray >
{
	enum
	{
		WithNetDeltaSerializer = true,
	};
};

And the relevant inventory component code:

UInventoryComponent::UInventoryComponent(const FObjectInitializer& OI)
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = false;
 
	SetIsReplicatedByDefault(true);
 
	Slots.Items.Empty();
 
	for (int i = 0; i < NumberOfSlots; i++)
	{
		CreateEmptySlot();
	}
}
 
// Called when the game starts
void UInventoryComponent::BeginPlay()
{
	Super::BeginPlay();
}
 
void UInventoryComponent::OnUnregister()
{
	const AActor* lOwner = GetOwner();
	if (lOwner && lOwner->HasAuthority())
	{
		DestroyAllSlots();
	}
 
	Super::OnUnregister();
}
 
    #if WITH_EDITOR
void UInventoryComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	Super::PostEditChangeProperty(PropertyChangedEvent);
 
	FName PropertyName = (PropertyChangedEvent.Property != NULL) ? PropertyChangedEvent.Property->GetFName() : NAME_None;
 
	//UE_LOG(LogTemp, Warning, TEXT("%s() - %s"), *FString(__FUNCTION__), *PropertyName.ToString());
 
	if (PropertyName == TEXT("Items"))
	{
		//Slots.MarkArrayDirty();
		//OnInventoryUpdated.Broadcast();
	}
 
	if (PropertyName == TEXT("NumberOfSlots"))
	{
		Slots.Items.Empty();
 
		for (int i = 0; i < NumberOfSlots; i++)
		{
			CreateEmptySlot();
		}
	}
}
#endif //WITH_EDITOR
 
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	DOREPLIFETIME(UInventoryComponent, Slots);
}
 
bool UInventoryComponent::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
	bool bWroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
	return bWroteSomething;
}
 
 
FInventoryArray UInventoryComponent::GetAllSlots()
{
	return Slots;
}
 
void UInventoryComponent::CreateEmptySlot()
{
	if (!GetOwner())
	{
		return;
	}
 
	if (GetOwner() && GetOwner()->GetLocalRole() == ROLE_AutonomousProxy)
	{
		CreateEmptySlot_Server();
		return;
	}
 
	FInventoryItem CreatedSlot;
	const int32 EmptyIndex = Slots.Items.Add(CreatedSlot);
 
	Slots.MarkArrayDirty();
 
	OnInventoryUpdated.Broadcast();
}
 
void UInventoryComponent::CreateEmptySlot_Server_Implementation()
{
	CreateEmptySlot();
}
 
bool UInventoryComponent::CreateEmptySlot_Server_Validate()
{
	return true;
}
 
bool UInventoryComponent::DestroySlot(FInventoryItem ItemToDestroy)
{
	if (!GetOwner())
	{
		return false;
	}
 
	if (GetOwner() && GetOwner()->GetLocalRole() == ROLE_AutonomousProxy)
	{
		DestroySlot_Server(ItemToDestroy);
		return DestroySlot_Server_Validate(ItemToDestroy);
	}
 
	return true;
}
 
void UInventoryComponent::DestroySlot_Server_Implementation(FInventoryItem ItemToDestroy)
{
	DestroySlot(ItemToDestroy);
}
 
bool UInventoryComponent::DestroySlot_Server_Validate(FInventoryItem ItemToDestroy)
{
	return true;
}
 
void UInventoryComponent::DestroyAllSlots()
{
	if (!GetOwner())
	{
		return;
	}
 
	if (GetOwner() && GetOwner()->GetLocalRole() == ROLE_AutonomousProxy)
	{
		DestroyAllSlots_Server();
		return;
	}
 
	Slots.Items.Empty();
}
 
void UInventoryComponent::DestroyAllSlots_Server_Implementation()
{
	DestroyAllSlots();
}
 
bool UInventoryComponent::DestroyAllSlots_Server_Validate()
{
	return true;
}

Is there something else I should be overriding or implementing to not get this error?

I even got the LyraStarterGame to see if I was initializing my structs wrong but I am stumped.

Here is what the Character blueprint looks like:

Any help is appreciated.

Thanks!

Call MarkItemDirty on the array with the item when you add or update an item.
When you remove an item, call MarkArrayDirty.
The naming makes it seem like you need to call MarkArrayDirty when you don’t…

That seems to have corrected the issue. I have been able to extend this a bit more since then. Thank you!