Variable Replication Order

Is there anyway to force the replication order for variables?

I am getting a race condition at line 190 between AbilityStates and ActiveAbility.

If it hits line 190 before AbilityStates is replicated then the Phase will be Active.

If it hits it after then the phase will be Cooldown.

// ** FPSAbilityComponent.h **
UENUM(BlueprintType)
enum class EFPSAbilityPhase : uint8
{
    Ready,
    Start,
    Active,
    Cooldown,
    Disabled
};

USTRUCT(BlueprintType)
struct FFPSAbilityState
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
    EFPSAbilityPhase Phase;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
    float Charges;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
    float MaxCharges;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
    float Cooldown;

    // Duration Remaining
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AbilityState)
    float Duration;
};

USTRUCT()
struct FFPSActiveAbility
{
    GENERATED_BODY()

    UPROPERTY()
    uint8 ID;

    UPROPERTY()
    FFPSAbilityAnimation Animation;

    UPROPERTY()
    uint8 ForceRep : 1;
};

UCLASS( ClassGroup=(Abilities), meta=(BlueprintSpawnableComponent) )
class FPSGAME_API UFPSAbilityComponent : public UActorComponent
{
    GENERATED_BODY()

public:

    // Called when the game starts
    virtual void BeginPlay() override;
	
	UFUNCTION()
    void OnRep_ActiveAbility();
	
	UFUNCTION()
    void OnRep_AbilityStates();
	
	
	// Set of abilities
	UPROPERTY(Transient, Replicated, BlueprintReadOnly, Category = Abilities)
	class UFPSAbilitySet* AbilitySet;

	UPROPERTY(EditDefaultsOnly, Category = Abilities)
	TArray<TSubclassOf<class UFPSAbility>> DefaultAbilityClasses;

	UPROPERTY(Transient, Replicated)
	TArray<class UFPSAbility*> Abilities;

	// Replicated ability state
	UPROPERTY(Transient, VisibleAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_AbilityStates, Category = Ability)
	TArray<FFPSAbilityState> AbilityStates;

	UPROPERTY(Transient, ReplicatedUsing = OnRep_ActiveAbility)
	FFPSActiveAbility ActiveAbility;
}

// ** FPSAbilityComponent.cpp**
void UFPSAbilityComponent::BeginPlay()
{
    Super::BeginPlay();

    if (GetOwner() && GetOwner()->HasAuthority())
    {
        // Create the AbilitySet
        AbilitySet = NewObject<UFPSAbilitySet>(this, DefaultAbilitySetClass);

        for (int i = 0; i < AbilitySet->DefaultAbilityClasses.Num(); i++)
        {
            // Create the Ability and set it's Outer
            UFPSAbility* Ability = NewObject<UFPSAbility>(this, AbilitySet->DefaultAbilityClasses[i]);
            Ability->ID = i;

            // Add the Ability to the AbilitySet
            AbilitySet->Abilities.Add(Ability);

            // Create the AbilityStates
            FFPSAbilityState AbilityState;
            // Set the initial property values
            AbilityState.Charges = Ability->Charges;
            AbilityState.MaxCharges = Ability->MaxCharges;
            AbilityState.Cooldown = Ability->Cooldown;
			AbilityState.Duration = Ability->Duration;
            AbilityState.Phase = EFPSAbilityPhase::Ready;

            // Add the AbilityState to the Array
            AbilityStates.Add(AbilityState);
        }
    }
}

void UFPSAbilityComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(UFPSAbilityComponent, AbilityStates);

    // Replicate the Ability Set to everyone
    // The Ability Set is responsible for finer replication granularity
    DOREPLIFETIME(UFPSAbilityComponent, AbilitySet);

    // Replicate the Active Ability to everyone except owner
    // The owner already has the Abilities since they are replicated to him
    DOREPLIFETIME_CONDITION(UFPSAbilityComponent, ActiveAbility, COND_SkipOwner);
}

void UFPSAbilityComponent::StartAbility(int32 Index)
{
    // Local simulation for owning client
    if (GetOwner() && Cast<AFPSCharacter>(GetOwner())->IsLocallyControlled())
    {
        if (AbilitySet)
        {
            if (AbilityStates.IsValidIndex(Index) && AbilityStates[Index].Phase == EFPSAbilityPhase::Ready 
                && AbilityStates[ActiveAbility.ID].Phase != EFPSAbilityPhase::Start && AbilityStates[ActiveAbility.ID].Phase != EFPSAbilityPhase::Active)
            {
                LocalActiveAbilityID = Index;

                SimulateAbilityLocally(Index);
            }
        }
    }

    ServerStartAbility(Index);
}

bool UFPSAbilityComponent::ServerStartAbility_Validate(int32 Index)
{
    return true;
}

void UFPSAbilityComponent::ServerStartAbility_Implementation(int32 Index)
{
    if (AbilitySet && AbilitySet->Abilities.IsValidIndex(Index) && AbilitySet->Abilities[Index] != nullptr)
    {
        if (AbilityStates[Index].Phase == EFPSAbilityPhase::Ready
                && AbilityStates[ActiveAbility.ID].Phase != EFPSAbilityPhase::Start && AbilityStates[ActiveAbility.ID].Phase != EFPSAbilityPhase::Active)
        {
            ConsumeCharge(Index);
            AbilitySet->Abilities[Index]->Start();
        }
    }
}

void UFPSAbilityComponent::SimulateAbility(int32 Index)
{
    // Animation
    if (ActiveAbility.Animation.Pawn3P)
    {
        AFPSCharacter* Character = Cast<AFPSCharacter>(GetOwner());
        // Play the Third Person Animation
        Character->PlayAnimMontage(ActiveAbility.Animation.Pawn3P);
        // Play the First Person Animation
        Character->PlayAnimMontageFP(ActiveAbility.Animation.Pawn1P);
    }

    // Create the Ability since we aren't replicating to other clients (simulated proxies)
    UFPSAbility* Ability = NewObject<UFPSAbility>(this, AbilitySet->DefaultAbilityClasses[Index]);
    
    if (AbilityStates[Index].Phase == EFPSAbilityPhase::Start)
    {
        Ability->Simulate();
    }
    // ** RACE CONDITION, SOMETIMES PHASE IS ACTIVE AND SOMETIMES IT'S COOLDOWN
    else if (AbilityStates[Index].Phase == EFPSAbilityPhase::Active || AbilityStates[Index].Phase == EFPSAbilityPhase::Cooldown)
    {
        Ability->SimulateExecute();
    }
}

void UFPSAbilityComponent::SimulateAbilityLocally(int32 Index)
{
    if (AbilitySet)
    {
        AFPSCharacter* Character = Cast<AFPSCharacter>(GetOwner());

        // Play the Third Person Animation
        Character->PlayAnimMontage(AbilitySet->Abilities[Index]->Animation.Pawn3P);
        // Play the First Person Animation
        Character->PlayAnimMontageFP(AbilitySet->Abilities[Index]->Animation.Pawn1P);

        if (AbilitySet->Abilities[Index]->bExecuteOnStart)
        {
            AbilitySet->Abilities[Index]->SimulateExecute();
        }
        else
        {
            AbilitySet->Abilities[Index]->Simulate();
        }
    }
}

void UFPSAbilityComponent::ConsumeCharge(int32 Index)
{
    AbilityStates[Index].Charges--;
}

void UFPSAbilityComponent::OnRep_AbilityStates()
{
}

void UFPSAbilityComponent::OnRep_ActiveAbility()
{
    SimulateAbility(ActiveAbility.ID);
}

void UFPSAbilityComponent::SetActiveAbility(int32 AbilityID, FFPSAbilityAnimation Animation)
{
    ActiveAbility.ID = AbilityID;
    ActiveAbility.Animation = Animation;
}

// ** FPSAbility.h **
UCLASS(Blueprintable, BlueprintType)
class FPSGAME_API UFPSAbility : public UObject
{
    GENERATED_BODY()
    
public:
	UFUNCTION(BlueprintCallable, Category = Ability)
    void Start();

    UFUNCTION(BlueprintCallable, Category = Ability)
    void Execute();

    UFUNCTION(BlueprintCallable, Category = Ability)
    void End();

    UFUNCTION(BlueprintImplementableEvent, Category = Ability)
    void Simulate();

    UFUNCTION(BlueprintImplementableEvent, Category = Ability)
    void SimulateExecute();

    UFUNCTION(BlueprintImplementableEvent, Category = Ability)
    void OnStart();

    // Blueprint implementable version of Execute
    UFUNCTION(BlueprintImplementableEvent, Category = Ability)
    void OnExecute();

    UFUNCTION(BlueprintImplementableEvent, Category = Ability)
    void OnEnd();
	
	// Number of charges that can be stored at a time
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
    float Charges;

    // Max number of charges that a player can hold at a time (0 for infinite)
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
    float MaxCharges;

    // Number of seconds before this ability can be recast (0 for none)
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
    float Cooldown;

    // How charges are restored after use
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Ability)
    EFPSAbilityChargeMode ChargeMode;

    // Animation to be played when this ability is executed
    UPROPERTY(EditDefaultsOnly, Category = Animation)
    FFPSAbilityAnimation Animation;

    UPROPERTY()
    int32 ID;

    // Cache the casted AbilityComponent we belong to
    UPROPERTY(BlueprintReadOnly, Category = Ability)
    class UFPSAbilityComponent* AbilityComponent;

    // Whether it should call Execute on Start
    UPROPERTY(EditDefaultsOnly, Category = Ability)
    bool bExecuteOnStart;

    UPROPERTY(EditDefaultsOnly, Category = Ability)
    float Duration;

}

// ** FPSAbility.cpp **
void UFPSAbility::Start()
{
    // Update Phase
    if (AbilityComponent)
    {
        AbilityComponent->AbilityStates[ID].Phase = EFPSAbilityPhase::Start;
    }

    // Instant
    if (bExecuteOnStart)
    {
        Execute();

        AbilityComponent->SetActiveAbility(ID, Animation);
    }
    // Animation
    else if (Animation.Pawn3P)
    {
        AbilityComponent->SetActiveAbility(ID, Animation);
        // Manually call OnRep for the Server
        AbilityComponent->OnRep_ActiveAbility();
    }

    // Force replication, so the same ability can be called multiple times in a row
    AbilityComponent->ActiveAbility.ForceRep = AbilityComponent->ActiveAbility.ForceRep >= 1 ? 0 : AbilityComponent->ActiveAbility.ForceRep + 1;

    OnStart();
}

void UFPSAbility::Execute()
{
    // Update Phase
    if (AbilityComponent)
    {
        AbilityComponent->AbilityStates[ID].Phase = EFPSAbilityPhase::Active;
    }

    // Call the blueprint event
    OnExecute();
}

void UFPSAbility::End()
{
    OnEnd();

    // Update phase
    if (AbilityComponent)
    {
        AbilityComponent->AbilityStates[ID].Phase = EFPSAbilityPhase::Cooldown;
    }
}

// ** FPSAbilitySet.cpp **
void UFPSAbilitySet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    // Replicate Abilities to owning client only
    DOREPLIFETIME_CONDITION(UFPSAbilitySet, Abilities, COND_OwnerOnly);

    // Replicate Ability Classes to other clients, so they can create the active one when needed
    DOREPLIFETIME_CONDITION(UFPSAbilitySet, DefaultAbilityClasses, COND_SkipOwner);
}

As I understand it, UObject property replication is reliable, but it is neither atomic nor is it in guaranteed order. To work around it, I believe you can create a struct that contains the properties that you need to have replicated atomically.

In this example, that would mean putting both AbilityStates and ActiveAbility in a new USTRUCT() struct FAbilityData, and then putting that FAbilityData as a replicated variable inside UFPSAbilityComponent.