I believe I’ve found an edge case for Push Model Replication that causes Actors to be in the incorrect Network Role when swapping possession. This is similar to UE-66313, but only when Push Model is enabled and Actors with full Push Model support are involved.
A fix was made in 5.4 for this issue here, but the bForceCompareProperties flag is not respected for objects that have full Push Model support. This happens in CompareParentProperties()in “RepLayout.cpp” for the branch considering the case where ERepLayoutFlags::FullPushProperties is true for the object.
The issue requires multiple network connections to repro. It does not reproduce for the first client connection–only for other connections. The steps are as follows:
- Launch a PIE session with at least 2 clients and the ability to change possession.
- The Actors involved should have full Push Model support, and Push Model should be enabled.
- On the second client, swap possession to a different pawn.
- Observe the previously-possessed pawn has the role
ROLE_AutonomousProxyinstead of the expectedROLE_SimulatedProxy.
The reason this only happens on the second client is subtle. The outline of the problem is roughly as follows:
- On a possession swap,
AActor::SetAutonomousProxy()is used to change the network role when the Actor is unpossessed. This callsAActor::ForcePropertyCompare(), which should ensure all properties are checked–regardless of the dirty state of properties for that frame, essentially disabling shared shadow state. - On the first connection during replication, the previously-possessed Actor has all properties compared. Prior to the swap,
FScopedRoleDowngradewould have downgrade the role to the first connection (since the Actor is owned by the second connection). In~FScopedRoleDowngrade(), the role would be restored after replication and theRemoteRoleproperty would be marked dirty. However, now that the Actor has changed its role toROLE_SimulatedProxy, no downgrade happens andRemoteRoleis not marked dirty at the end of replication to the first connection. - Once replication to the first connection finishes, the dirty state is cleared in
CompareParentProperties()usingSharedParams.PushModelState->ResetDirtyStates(). Now the Actor has no dirty properties. - Next, replication starts on the second connection. Since the Actor has full Push Model support, it takes the branch in
CompareParentProperties()whereEnumHasAnyFlags(SharedParams.Flags, ERepLayoutFlags::FullPushProperties)is true. - [BUG] This is where the trouble is. This branch only iterates over dirty properties by calling
SharedParams.PushModelState->GetDirtyProperties(), which bypassesIsPropertyDirty(). The flagbForceComparePropertiesis not respected. This meansCompareRoleProperty()never gets called to check if the saved remote role has changed from the Actors new role. SeeSavedRemoteRoleinFSendingRepState. - Now, the previously-possessed Actor has the wrong network role.
My proposed fix is to update CompareParentProperties() in “RepLayout.cpp” to have the following:
// Old
if (UNLIKELY(SharedParams.bForceFail))
{
...
}
// New
if (UNLIKELY(SharedParams.bForceFail || SharedParams.bForceCompareProperties))
{
...
}
This feels like an opportunity to simplify IsPropertyDirty() in “RepLayout.cpp” to remove the check for bForceCompareProperties. The only code path that looks like it may be circumvented would be the validation path with WITH_PUSH_VALIDATION_SUPPORT.