Actor property replication issue when property set to class default

I’m having a weird issue with replication and wondered if it’s just because I’m doing something unexpected that you’re not supposed to.

I’ve made a super basic example class:
#pragma once
#include “CoreMinimal.h”
#include “GameFramework/Actor.h”
#include “ReplicationTest.generated.h”

UCLASS( ClassGroup=(TestActor), Blueprintable, BlueprintType)
class MVENGINE_API AReplicationTest : public AActor
{
    GENERATED_BODY()

public:
    // Sets default values for this actor's properties
    AReplicationTest();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

    // Replication notification for locking
    UFUNCTION()
    virtual void OnRep_Replication();

public:
    UPROPERTY(ReplicatedUsing=OnRep_Replication, BlueprintReadWrite, EditAnywhere)
    int32 ReplicationTest = 0;
    UPROPERTY(BlueprintReadOnly, EditAnywhere)
    USceneComponent* Root;
};

With an implementation that lets me debug things:
#include “ReplicationTest.h”
#include “Net/UnrealNetwork.h”

// Sets default values
AReplicationTest::AReplicationTest()
{
    UE_LOG(LogTemp, Display, TEXT("%s [Client %i] %s (ReplicationTest: %i)"), __FUNCTIONW__, GPlayInEditorID, *GetName(), ReplicationTest);
    Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    RootComponent = Root;
    SetReplicates(true);
}

void AReplicationTest::BeginPlay()
{
    UE_LOG(LogTemp, Display, TEXT("%s [Client %i] %s (ReplicationTest: %i)"), __FUNCTIONW__, GPlayInEditorID, *GetName(), ReplicationTest);
    Super::BeginPlay();
}

void AReplicationTest::OnRep_Replication()
{
    UE_LOG(LogTemp, Display, TEXT("%s [Client %i] %s (ReplicationTest: %i)"), __FUNCTIONW__, GPlayInEditorID, *GetName(), ReplicationTest);
}

void AReplicationTest::GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const
{
    UE_LOG(LogTemp, Display, TEXT("%s [Client %i] %s (ReplicationTest: %i)"), __FUNCTIONW__, GPlayInEditorID, *GetName(), ReplicationTest);
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(AReplicationTest, ReplicationTest);
}

I’ve then added three instances of this actor to a map, ReplicationValue0, ReplicationValue1 and ReplicationValue2 and I set their ‘ReplicationTest’ values in the editor, Details panel to 0, 1 and 2 respectively.

Next I create a level blueprint to do this on just the server:

and I run this as a dedicated server and a single player in editor, Everything works as expected - here’s the console with a filter for anything with ‘AReplicationTest’

LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue2 (ReplicationTest: 2)
LogBlueprintUserMessages: [ReplicationTest_C_1] Server: AReplicationTest setup
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 1] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 2] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue2 (ReplicationTest: 5)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue1 (ReplicationTest: 4)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue0 (ReplicationTest: 3)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue0 (ReplicationTest: 3)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue1 (ReplicationTest: 4)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue2 (ReplicationTest: 5)

You can see that the new player (Client 2) that joins the server (Client 1) receives the three OnRep_Replication’s before BeginPlay as the values have changed on the server. The client reports the correct 3, 4, 5 values for the respective AReplicationTest actors

However, I then change the blueprint to this:

What I’m expecting to happen is that the all of the actor’s ReplicationTest values are set to 0 on the server, and when the client joins it receives two OnRep_Replication’s after the values are set to 0 (for the two that had ReplicationTest set to 1 and 2). However, here’s the log from that run:

LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue2 (ReplicationTest: 2)
LogBlueprintUserMessages: [ReplicationTest_C_3] Server: AReplicationTest setup
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 1] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 2] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue2 (ReplicationTest: 2)

You can see here that OnRep_Replication is never called on the client, and it never receives the values being set to 0. There is now a difference between the server and client, as the three actors have ReplicationTest’s of 0,0,0 on the server and 0,1,2 on the client.

Tweaking bits of code here and there to see if I could narrow down if I’m doing something, wrong, I noticed that if I change the class definition so that this:
int32 ReplicationTest = 0;
is this
int32 ReplicationTest = -1;
and then run with the same blueprint above (setting all three to 0), the log shows this:

LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue2 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue1 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue0 (ReplicationTest: -1)
LogBlueprintUserMessages: [ReplicationTest_C_3] Server: AReplicationTest setup
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue2 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue1 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue0 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 1] Default__ReplicationTest (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 2] Default__ReplicationTest (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue0 (ReplicationTest: 0)

and everything works fine. The client receives all three OnRep_Replication’s and the values once again match on the server and client.

So it seems like replication-on-join fails to occur if the current value is equal to either the class default, or the value loaded from the editor. I can also confirm this same behaviour with blueprints.

I’ve tried this in both 4.23, 4.24 and 4.25 and it appears this behaviour happens in all.

Is this just correct, expected behaviour? is there something special I have to do when I’m replicating a value that’s set to the class/BP default value? Have I done something incredibly dumb somewhere in my code (I’m not that experienced with C++ replication so not 100% sure)?

I’ve attached a 4.25 blueprint version of this issue with the 3/4/5 and 0/0/0 example maps above:
[ReplicationTesting.zip][3]

Just to update this - still happening on 4.26.