FFastArraySerializer won't replicate changes

I’m using the FFastArraySerializer to replicate many locations but it just doesn’t seem to replicate the changes. It replicates when an item is first added, but nothing after that. I’m not seeing updates on the client whilst outputting the array continually. I’ve tried many ways to change an item but the only thing I can get working is ‘Add_GetRef’. EmplaceAt_GetRef just seems to always add even if you specify an index. I’m either massively confused about how this is supposed to work or some other setting somewhere is off. Any advice from those who’ve been here before is much appreciated!

Here’s my code for the Item & Array.

struct FEnemyReplicatedLocationHealthArray;

USTRUCT(BlueprintType)
struct FEnemyReplicatedLocationHealth : public FFastArraySerializerItem
{
	GENERATED_BODY()

	FEnemyReplicatedLocationHealth()
	{}

	FEnemyReplicatedLocationHealth(
		int32 InID,
		FVector InLocation,
		float InHealth)
		: ID(InID)
		, Location(InLocation)
		, Health(InHealth)
	{
	}

	FString GetDebugString() const;
	
	friend FEnemyReplicatedLocationHealthArray;

	UPROPERTY()
	int32 ID;
	
	UPROPERTY()
	FVector Location;
	
	UPROPERTY()
	float Health;
};

USTRUCT(BlueprintType)
struct FEnemyReplicatedLocationHealthArray : public FFastArraySerializer
{
	GENERATED_BODY()

	FEnemyReplicatedLocationHealthArray()
	//	: Owner(nullptr)
	{
	}
	
	void Add(
		int32 InID,
		FVector InLocation,
		float InHealth);

	void Update(
		int32 InID,
		FVector InLocation,
		float InHealth);
	
	void RemoveEnemy(int32 InID);

	FEnemyReplicatedLocationHealth* GetEnemy(int32 InID);
	//~FFastArraySerializer contract
	void PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize);
	void PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize);
	void PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize);
	//~End of FFastArraySerializer contract

	bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
	{
		UE_LOG(LogTemp, Warning, TEXT("NetDeltaSerialize Called"))
		return FFastArraySerializer::FastArrayDeltaSerialize<FEnemyReplicatedLocationHealth, FEnemyReplicatedLocationHealthArray>(Items, DeltaParms, *this);
	}

	TArray<FEnemyReplicatedLocationHealth>& GetEnemies();

	TMap<int32, int32> IDtoIndexMap;
	
	UPROPERTY()
	TArray<FEnemyReplicatedLocationHealth> Items;
};

template<>
struct TStructOpsTypeTraits<FEnemyReplicatedLocationHealthArray> : public TStructOpsTypeTraitsBase2<FEnemyReplicatedLocationHealthArray>
{
	enum
	{
		WithNetDeltaSerializer = true,
	};
};

I’ve tried many things with the update .cpp - even removing and adding again instead of changing an entry.


//////////////////////////////////////////////////////////////////////
// FEnemyReplicatedProperties

FString FEnemyReplicatedLocationHealth::GetDebugString() const
{
	return FString::Printf(TEXT("--- FEnemyReplicatedProperties::GetDebugString() ---"));
	// return FString::Printf(TEXT("%sx%d"), *Tag.ToString(), StackCount);

}

//////////////////////////////////////////////////////////////////////
// FGameplayTagStackContainer

void FEnemyReplicatedLocationHealthArray::Add(
		int32 InID,
		FVector InLocation,
		float InHealth)
{
	const FEnemyReplicatedLocationHealth NewEnemy(InID, InLocation, InHealth);
	MarkItemDirty(Items.Add_GetRef(NewEnemy));
	IDtoIndexMap.Add(InID, Items.Num()-1);
}

void FEnemyReplicatedLocationHealthArray::Update(
		int32 InID,
		FVector InLocation,
		float InHealth)
{
	for (FEnemyReplicatedLocationHealth& Enemy : Items)
	{
		if (Enemy.ID == InID)
		{
			const FVector NewLocation = InLocation;
			const float NewHealth = InHealth;

			Enemy.Location = NewLocation;
			Enemy.Health = NewHealth;

			MarkItemDirty(Enemy);
		}
	}
}

void FEnemyReplicatedLocationHealthArray::RemoveEnemy(int32 InID)
{
	
	for (auto It = Items.CreateIterator(); It; ++It)
	{
		FEnemyReplicatedLocationHealth& Enemy = *It;
		if (Enemy.ID == InID)
		{
			It.RemoveCurrent();
			MarkArrayDirty();
		}
	}
}

FEnemyReplicatedLocationHealth* FEnemyReplicatedLocationHealthArray::GetEnemy(int32 InID)
{
	FEnemyReplicatedLocationHealth* EnemyFound = Items.FindByPredicate([InID](const FEnemyReplicatedLocationHealth& Enemy) {
		return Enemy.ID == InID;
	});
	if(EnemyFound)
	{
		return EnemyFound;
	}
	return nullptr;
}

void FEnemyReplicatedLocationHealthArray::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)
{
	for (int32 Index : RemovedIndices)
	{
		const FEnemyReplicatedLocationHealth& Enemy = Items[Index];
		IDtoIndexMap.Remove(Enemy.ID);
	}
}

void FEnemyReplicatedLocationHealthArray::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
	for (int32 Index : AddedIndices)
	{
		// UE_LOG(LogTemp, Warning, TEXT("FEnemyReplicatedLocationHealthArray Indexes Added %d"), Index);
		// const FEnemyReplicatedLocationHealth& Enemy = Enemies[Index];
		// IDtoIndexMap.Add(Enemy.ID, Index);
	}
}

void FEnemyReplicatedLocationHealthArray::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)
{
	for (int32 Index : ChangedIndices)
	{
		// UE_LOG(LogTemp, Warning, TEXT("FEnemyReplicatedLocationHealthArray Indexes Change %d"), Index);
		// const FEnemyReplicatedLocationHealth& Enemy = Enemies[Index];
		// IDtoIndexMap[Enemy.ID] = Index;
	}
}

TArray<FEnemyReplicatedLocationHealth>& FEnemyReplicatedLocationHealthArray::GetEnemies()
{
	return Items;
}