Mass Replication ProcessClientReplication and other issues

Hello everyone! I’m learning Mass Entity through a project I’m working on in my free time. So far, it’s been going pretty well. I upgraded to 5.8 because I saw that there were a lot of positive changes to the Mass Entity system, and I found a couple of issues that you may already be aware of, so I wanted to share them here.

First, I think this one is already known: I have to disable UseProcessingQueue to avoid a MassReplication check assertion caused by FQueryInfo checking the query by pointer instead of using an ID.

However, I found another issue related to removing replicated entities. The code that removes the entity from the array is located in a child class of UMassReplicatorBase that I created, following the same logic as the Crowd replication implementation:

auto RemoveEntityCallback = [&RepSharedFrag](FMassExecutionContext& Context, const FMassReplicatedAgentHandle Handle, const FMassClientHandle ClientHandle)
{
    ADAMassZombieClientBubbleInfo& UnitBubbleInfo = RepSharedFrag->GetTypedClientBubbleInfoChecked<ADAMassZombieClientBubbleInfo>(ClientHandle);

    UnitBubbleInfo.GetUnitSerializer().Bubble.RemoveAgentChecked(Handle);
};

CalculateClientReplication<FDAMassZombieFastArrayItem>(Context, ReplicationContext, CacheViewsCallback, AddEntityCallback, ModifyEntityCallback, RemoveEntityCallback);

The ReplicationProcessor also calls ProcessClientReplication by iterating over shared fragments.

QUICK_SCOPE_CYCLE_COUNTER(UMassReplicationProcessor_ProcessClientReplication);
FMassReplicationContext ReplicationContext(*World, LODSubsystem, *ReplicationSubsystem);
EntityManager.ForEachSharedFragment<FMassReplicationSharedFragment>([&EntityManager, &Context, &ReplicationContext, &ClientHandle](FMassReplicationSharedFragment& RepSharedFragment)
{
    RepSharedFragment.CurrentClientHandle = ClientHandle;

    RepSharedFragment.EntityQuery.ForEachEntityChunk(Context, [&ReplicationContext, &RepSharedFragment](FMassExecutionContext& Context)
    {
       RepSharedFragment.CachedReplicator->ProcessClientReplication(Context, ReplicationContext);
    });
});

The issue is that when you remove the last entity, that query no longer returns anything. Since there are no entities, there is no shared fragment to process, so ProcessClientReplication never gets called. This eventually causes the final check in the MassProcessor to fail.

for (FMassReplicationAgentDataMap::TIterator It = ClientReplicationInfo.AgentsData.CreateIterator(); It; ++It)
{
    FMassReplicatedAgentData& AgentData = It.Value();
    if (AgentData.bPendingDestruction || AgentData.LOD == EMassLOD::Off)
    {
       checkf(!AgentData.Handle.IsValid(), TEXT("This replicated agent should have been removed from this client and was not"));
       It.RemoveCurrent();
    }
}

For now, I can work around this by keeping a dummy entity isolated somewhere in the world, but I wanted to let you know that this is happening as well.

On another note, I also wanted to take this opportunity to say that so far I think the framework is pretty cool. It may still have some bugs and need a bit more time to mature, but so far I’ve found it quite straightforward to use.

[Attachment Removed]

Steps to Reproduce
Remove the last replication entity from the game

[Attachment Removed]

Short update, i was able to “fix it”, not sure if its the proper way, doing another virtual method on UMassReplicatorBase:

class UMassReplicatorBase : public UObject
{
	GENERATED_BODY()
 
public:
	/**
	 * Must override to add specific entity query requirements for replication
	 * Usually we add replication processor handler requirements
	 */
	virtual void AddRequirements(FMassEntityQuery& EntityQuery) PURE_VIRTUAL(UMassReplicatorBase::ConfigureQueries, );
 
	/**
	 * Must override to process the client replication
	 * This methods should call CalculateClientReplication with the appropriate callback implementation
	 */
	virtual void ProcessClientReplication(FMassExecutionContext& Context, FMassReplicationContext& ReplicationContext) PURE_VIRTUAL(UMassReplicatorBase::ProcessClientReplication, );
 
	virtual void ProcessPendingRemovals(const FMassClientHandle& CurrentClientHandle, FMassReplicationContext& ReplicationContext, FMassReplicationSharedFragment& ReplicationSharedFragment) PURE_VIRTUAL(UMassReplicatorBase::ProcessPendingRemovals, );
protected:
	/**
	 *  Implemented as straight template callbacks as when profiled this was faster than TFunctionRef. Its probably easier to pass Lamdas in to these
	 *  but Functors can also be used as well as TFunctionRefs etc. Its also fairly straight forward to call member functions via some Lamda glue code
	 */
	template<typename AgentArrayItem, typename CacheViewsCallback, typename AddEntityCallback, typename ModifyEntityCallback, typename RemoveEntityCallback>
	static void CalculateClientReplication(FMassExecutionContext& Context, FMassReplicationContext& ReplicationContext, CacheViewsCallback&& CacheViews
		, AddEntityCallback&& AddEntity, ModifyEntityCallback&& ModifyEntity, RemoveEntityCallback&& RemoveEntity);
 
	template<typename RemoveEntityCallback>
	static void CalculatePendingRemovals(FMassReplicationContext& ReplicationContext, const FMassClientHandle& ClientHandle, RemoveEntityCallback&& RemoveEntity);
};

and then on MassReplicationProcessor line 294

		{
			QUICK_SCOPE_CYCLE_COUNTER(UMassReplicationProcessor_LODAdjustDistance);
			FMassReplicationContext ReplicationContext(*World, LODSubsystem, *ReplicationSubsystem);
 
			EntityManager.ForEachSharedFragment<FMassReplicationSharedFragment>([&ClientHandle, &ReplicationContext](FMassReplicationSharedFragment& RepSharedFragment)
			{
				RepSharedFragment.bHasAdjustedDistancesFromCount = RepSharedFragment.LODCalculator.AdjustDistancesFromCount();
 
				RepSharedFragment.CurrentClientHandle = ClientHandle;
				RepSharedFragment.CachedReplicator->ProcessPendingRemovals(ClientHandle, ReplicationContext, RepSharedFragment);
			});
		}

This calls my replicator and then there i can check on destroyed entities.

Let me know if its ok

[Attachment Removed]

I was not aware of this issue, but our guidance is to no longer use MassReplication that was shipped with UE5 anymore. If you need replication, we recommend the new MassIrisReplication (and there is a representation version) plugin that is in the UE6 Main stream. All of our efforts have gone into it rather than the older replication module which we ran into multiple problems with when working on Mass replication over the years.

-James

[Attachment Removed]