// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. /*============================================================================= RepLayout.cpp: Unreal replication layout implementation. =============================================================================*/ #include "EnginePrivate.h" #include "Net/RepLayout.h" #include "Net/DataReplication.h" #include "Net/NetworkProfiler.h" #include "Engine/ActorChannel.h" static TAutoConsoleVariable CVarAllowPropertySkipping( TEXT( "net.AllowPropertySkipping" ), 1, TEXT( "Allow skipping of properties that haven't changed for other clients" ) ); static TAutoConsoleVariable CVarDoPropertyChecksum( TEXT( "net.DoPropertyChecksum" ), 0, TEXT( "" ) ); FAutoConsoleVariable CVarDoReplicationContextString( TEXT( "net.ContextDebug" ), 0, TEXT( "" ) ); #define ENABLE_PROPERTY_CHECKSUMS //#define SANITY_CHECK_MERGES #define USE_CUSTOM_COMPARE //#define ENABLE_SUPER_CHECKSUMS #ifdef USE_CUSTOM_COMPARE static FORCEINLINE bool CompareBool( const FRepLayoutCmd & Cmd, const void * A, const void * B ) { return Cmd.Property->Identical( A, B ); } static FORCEINLINE bool CompareObject( const FRepLayoutCmd & Cmd, const void * A, const void * B ) { return Cmd.Property->Identical( A, B ); } template< typename T > bool CompareValue( const T * A, const T * B ) { return *A == *B; } template< typename T > bool CompareValue( const void * A, const void * B ) { return CompareValue( (T*)A, (T*)B); } static FORCEINLINE bool PropertiesAreIdenticalNative( const FRepLayoutCmd & Cmd, const void * A, const void * B ) { switch ( Cmd.Type ) { case REPCMD_PropertyBool: return CompareBool( Cmd, A, B ); case REPCMD_PropertyByte: return CompareValue( A, B ); case REPCMD_PropertyFloat: return CompareValue( A, B ); case REPCMD_PropertyInt: return CompareValue( A, B ); case REPCMD_PropertyName: return CompareValue( A, B ); case REPCMD_PropertyObject: return CompareObject( Cmd, A, B ); case REPCMD_PropertyUInt32: return CompareValue( A, B ); case REPCMD_PropertyUInt64: return CompareValue( A, B ); case REPCMD_PropertyVector: return CompareValue( A, B ); case REPCMD_PropertyVector100: return CompareValue( A, B ); case REPCMD_PropertyVectorQ: return CompareValue( A, B ); case REPCMD_PropertyVectorNormal: return CompareValue( A, B ); case REPCMD_PropertyVector10: return CompareValue( A, B ); case REPCMD_PropertyPlane: return CompareValue( A, B ); case REPCMD_PropertyRotator: return CompareValue( A, B ); case REPCMD_PropertyNetId: return CompareValue( A, B ); case REPCMD_RepMovement: return CompareValue( A, B ); case REPCMD_PropertyString: return CompareValue( A, B ); case REPCMD_Property: return Cmd.Property->Identical( A, B ); default: UE_LOG( LogNet, Fatal, TEXT( "PropertiesAreIdentical: Unsupported type! %i (%s)" ), Cmd.Type, *Cmd.Property->GetName() ); } return false; } static FORCEINLINE bool PropertiesAreIdentical( const FRepLayoutCmd & Cmd, const void * A, const void * B ) { const bool bIsIdentical = PropertiesAreIdenticalNative( Cmd, A, B ); #if 0 // Sanity check result if ( bIsIdentical != Cmd.Property->Identical( A, B ) ) { UE_LOG( LogNet, Fatal, TEXT( "PropertiesAreIdentical: Result mismatch! (%s)" ), *Cmd.Property->GetFullName() ); } #endif return bIsIdentical; } #else static FORCEINLINE bool PropertiesAreIdentical( const FRepLayoutCmd & Cmd, const void * A, const void * B ) { return Cmd.Property->Identical( A, B ); } #endif static FORCEINLINE void StoreProperty( const FRepLayoutCmd & Cmd, void * A, const void * B ) { Cmd.Property->CopySingleValue( A, B ); } static FORCEINLINE void SerializeGenericChecksum( FArchive & Ar ) { uint32 Checksum = 0xABADF00D; Ar << Checksum; check( Checksum == 0xABADF00D ); } static void SerializeReadWritePropertyChecksum( const FRepLayoutCmd & Cmd, const int32 CurCmdIndex, const uint8 * Data, FArchive & Ar, const bool bDiscard ) { if ( bDiscard ) { uint32 MarkerChecksum = 0; uint32 PropertyChecksum = 0; Ar << MarkerChecksum; Ar << PropertyChecksum; return; } // Serialize various attributes that will mostly ensure we are working on the same property const uint32 NameHash = GetTypeHash( Cmd.Property->GetName() ); uint32 MarkerChecksum = 0; // Evolve the checksum over several values that will uniquely identity where we are and should be MarkerChecksum = FCrc::MemCrc_DEPRECATED( &NameHash, sizeof( NameHash ), MarkerChecksum ); MarkerChecksum = FCrc::MemCrc_DEPRECATED( &Cmd.Offset, sizeof( Cmd.Offset ), MarkerChecksum ); MarkerChecksum = FCrc::MemCrc_DEPRECATED( &CurCmdIndex, sizeof( CurCmdIndex ), MarkerChecksum ); const uint32 OriginalMarkerChecksum = MarkerChecksum; Ar << MarkerChecksum; if ( MarkerChecksum != OriginalMarkerChecksum ) { // This is fatal, as it means we are out of sync to the point we can't recover UE_LOG( LogNet, Fatal, TEXT( "SerializeReadWritePropertyChecksum: Property checksum marker failed! [%s]" ), *Cmd.Property->GetFullName() ); } if ( Cmd.Property->IsA( UObjectPropertyBase::StaticClass() ) ) { // Can't handle checksums for objects right now // Need to resolve how to handle unmapped objects return; } // Now generate a checksum that guarantee that this property is in the exact state as the server // This will require NetSerializeItem to be deterministic, in and out // i.e, not only does NetSerializeItem need to write the same blob on the same input data, but // it also needs to write the same blob it just read as well. FBitWriter Writer( 0, true ); Cmd.Property->NetSerializeItem( Writer, NULL, const_cast< uint8 * >( Data ) ); if ( Ar.IsSaving() ) { // If this is the server, do a read, and then another write so that we do exactly what the client will do, which will better ensure determinism // We do this to force InitializeValue, DestroyValue etc to work on a single item const int32 OriginalDim = Cmd.Property->ArrayDim; Cmd.Property->ArrayDim = 1; TArray< uint8 > TempPropMemory; TempPropMemory.AddZeroed( Cmd.Property->ElementSize + 4 ); uint32 * Guard = (uint32*)&TempPropMemory[TempPropMemory.Num() - 4]; const uint32 TAG_VALUE = 0xABADF00D; *Guard = TAG_VALUE; Cmd.Property->InitializeValue( TempPropMemory.GetTypedData() ); check( *Guard == TAG_VALUE ); // Read it back in and then write it out to produce what the client will produce FBitReader Reader( Writer.GetData(), Writer.GetNumBits() ); Cmd.Property->NetSerializeItem( Reader, NULL, TempPropMemory.GetTypedData() ); check( Reader.AtEnd() && !Reader.IsError() ); check( *Guard == TAG_VALUE ); // Write it back out for a final time Writer.Reset(); Cmd.Property->NetSerializeItem( Writer, NULL, TempPropMemory.GetTypedData() ); check( *Guard == TAG_VALUE ); // Destroy temp memory Cmd.Property->DestroyValue( TempPropMemory.GetTypedData() ); // Restore the static array size Cmd.Property->ArrayDim = OriginalDim; check( *Guard == TAG_VALUE ); } uint32 PropertyChecksum = FCrc::MemCrc_DEPRECATED( Writer.GetData(), Writer.GetNumBytes() ); const uint32 OriginalPropertyChecksum = PropertyChecksum; Ar << PropertyChecksum; if ( PropertyChecksum != OriginalPropertyChecksum ) { // This is a warning, because for some reason, float rounding issues in the quantization functions cause this to return false positives UE_LOG( LogNet, Warning, TEXT( "Property checksum failed! [%s]" ), *Cmd.Property->GetFullName() ); } } uint16 FRepLayout::CompareProperties_r( const int32 CmdStart, const int32 CmdEnd, const uint8 * RESTRICT CompareData, const uint8 * RESTRICT Data, TArray< uint16 > & Changed, uint16 Handle ) const { for ( int32 CmdIndex = CmdStart; CmdIndex < CmdEnd; CmdIndex++ ) { const FRepLayoutCmd & Cmd = Cmds[CmdIndex]; check( Cmd.Type != REPCMD_Return ); Handle++; if ( Cmd.Type == REPCMD_DynamicArray ) { // Once we hit an array, start using a stack based approach CompareProperties_Array_r( CompareData ? CompareData + Cmd.Offset : NULL, (const uint8*)Data + Cmd.Offset, Changed, CmdIndex, Handle ); CmdIndex = Cmd.EndCmd - 1; // The -1 to handle the ++ in the for loop continue; } if ( CompareData == NULL || !PropertiesAreIdentical( Cmd, (const void*)( CompareData + Cmd.Offset ), (const void*)( Data + Cmd.Offset ) ) ) { Changed.Add( Handle ); } } return Handle; } void FRepLayout::CompareProperties_Array_r( const uint8 * RESTRICT CompareData, const uint8 * RESTRICT Data, TArray< uint16 > & Changed, const uint16 CmdIndex, const uint16 Handle ) const { const FRepLayoutCmd & Cmd = Cmds[CmdIndex]; FScriptArray * CompareArray = (FScriptArray *)CompareData; FScriptArray * Array = (FScriptArray *)Data; const uint16 ArrayNum = Array->Num(); const uint16 CompareArrayNum = CompareArray ? CompareArray->Num() : 0; TArray< uint16 > ChangedLocal; uint16 LocalHandle = 0; Data = (uint8*)Array->GetData(); CompareData = CompareData ? (uint8*)CompareArray->GetData() : (uint8*)NULL; for ( int32 i = 0; i < ArrayNum; i++ ) { const int32 ElementOffset = i * Cmd.ElementSize; const uint8 * LocalCompareData = ( i < CompareArrayNum ) ? ( CompareData + ElementOffset ) : NULL; LocalHandle = CompareProperties_r( CmdIndex + 1, Cmd.EndCmd - 1, LocalCompareData, Data + ElementOffset, ChangedLocal, LocalHandle ); } if ( ChangedLocal.Num() ) { Changed.Add( Handle ); Changed.Add( ChangedLocal.Num() ); // This is so we can jump over the array if we need to Changed.Append( ChangedLocal ); Changed.Add( 0 ); } else if ( ArrayNum != CompareArrayNum ) { // If nothing below us changed, we either shrunk, or we grew and our inner was an array that didn't have any elements check( ArrayNum < CompareArrayNum || Cmds[CmdIndex + 1].Type == REPCMD_DynamicArray ); // Array got smaller, send the array handle to force array size change Changed.Add( Handle ); Changed.Add( 0 ); Changed.Add( 0 ); } } bool FRepLayout::CompareProperties( FRepState * RESTRICT RepState, const uint8 * RESTRICT CompareData, const uint8 * RESTRICT Data, TArray< FRepChangedParent > & OutChangedParents, const TArray< uint16 > & PropertyList ) const { bool PropertyChanged = false; const uint16 * RESTRICT FirstProp = PropertyList.GetTypedData(); const uint16 * RESTRICT LastProp = FirstProp + PropertyList.Num(); for ( const uint16 * RESTRICT pLifeProp = FirstProp; pLifeProp < LastProp; ++pLifeProp ) { //#if USE_NETWORK_PROFILER //const uint32 PropertyStartTime = GNetworkProfiler.IsTrackingEnabled() ? FPlatformTime::Cycles() : 0; //#endif const FRepParentCmd & ParentCmd = Parents[*pLifeProp]; // We store changed properties on each parent, so we can build a final sorted change list later TArray< uint16 > & Changed = OutChangedParents[*pLifeProp].Changed; check( Changed.Num() == 0 ); // Loop over the block of child properties that are children of this parent for ( int32 i = ParentCmd.CmdStart; i < ParentCmd.CmdEnd; i++ ) { const FRepLayoutCmd & Cmd = Cmds[i]; check( Cmd.Type != REPCMD_Return ); // REPCMD_Return's are markers we shouldn't hit if ( Cmd.Type == REPCMD_DynamicArray ) { // Once we hit an array, start using a recursive based approach CompareProperties_Array_r( CompareData + Cmd.Offset, Data + Cmd.Offset, Changed, i, Cmd.RelativeHandle ); i = Cmd.EndCmd - 1; // Jump past properties under array, we've already checked them (the -1 because of the ++ in the for loop) continue; } if ( !PropertiesAreIdentical( Cmd, (void*)( CompareData + Cmd.Offset ), (const void*)( Data + Cmd.Offset ) ) ) { // Add this properties handle to the change list Changed.Add( Cmd.RelativeHandle ); } } if ( Changed.Num() > 0 ) { // Something changed on this parent property PropertyChanged = true; } //NETWORK_PROFILER( GNetworkProfiler.TrackReplicateProperty( ParentCmd.Property, true, false, FPlatformTime::Cycles() - PropertyStartTime, ParentCmd.Property->ElementSize * 8, 0 ) ); } return PropertyChanged; } void FRepLayout::LogChangeListMismatches( const uint8 * Data, UActorChannel * OwningChannel, const TArray< uint16 > & PropertyList, FRepState * RepState1, FRepState * RepState2, const TArray< FRepChangedParent > & ChangedParents1, const TArray< FRepChangedParent > & ChangedParents2 ) const { FRepChangedPropertyTracker * ChangeTracker = RepState1->RepChangedPropertyTracker.Get(); UObject * Object = (UObject*)Data; const UNetDriver * NetDriver = OwningChannel->Connection->Driver; UE_LOG( LogNet, Warning, TEXT( "LogChangeListMismatches: %s" ), *Object->GetName() ); UE_LOG( LogNet, Warning, TEXT( " %i, %i, %i, %i" ), NetDriver->ReplicationFrame, ChangeTracker->LastReplicationFrame, ChangeTracker->LastReplicationGroupFrame, RepState1->LastReplicationFrame ); for ( int32 i = 0; i < PropertyList.Num(); i++ ) { const int32 Index = PropertyList[i]; const int32 Changed1 = ChangedParents1[Index].Changed.Num(); const int32 Changed2 = ChangedParents2[Index].Changed.Num(); if ( Changed1 || Changed2 ) { FString Changed1Str = Changed1 ? TEXT( "TRUE" ) : TEXT( "FALSE" ); FString Changed2Str = Changed2 ? TEXT( "TRUE" ) : TEXT( "FALSE" ); FString ExtraStr = Changed1 != Changed2 ? TEXT( "<--- MISMATCH!" ) : TEXT( "<--- SAME" ); UE_LOG( LogNet, Warning, TEXT( " Property changed: %s (%s / %s) %s" ), *Parents[Index].Property->GetName(), *Changed1Str, *Changed2Str, *ExtraStr ); FString StringValue1; FString StringValue2; FString StringValue3; Parents[Index].Property->ExportText_InContainer( Parents[Index].ArrayIndex, StringValue1, Data, NULL, NULL, PPF_DebugDump ); Parents[Index].Property->ExportText_InContainer( Parents[Index].ArrayIndex, StringValue2, RepState1->StaticBuffer.GetTypedData(), NULL, NULL, PPF_DebugDump ); Parents[Index].Property->ExportText_InContainer( Parents[Index].ArrayIndex, StringValue3, RepState2->StaticBuffer.GetTypedData(), NULL, NULL, PPF_DebugDump ); UE_LOG( LogNet, Warning, TEXT( " Values: %s, %s, %s" ), *StringValue1, *StringValue2, *StringValue3 ); } } } static bool ChangedParentsHasChanged( const TArray< uint16 > & PropertyList, const TArray< FRepChangedParent > & ChangedParents ) { for ( int32 i = 0; i < PropertyList.Num(); i++ ) { if ( ChangedParents[PropertyList[i]].Changed.Num() ) { return true; } } return false; } void FRepLayout::SanityCheckShadowStateAgainstChangeList( FRepState * RepState, const uint8 * Data, UActorChannel * OwningChannel, const TArray< uint16 > & PropertyList, FRepState * OtherRepState, const TArray< FRepChangedParent > & OtherChangedParents ) const { const uint8 * CompareData = RepState->StaticBuffer.GetTypedData(); UObject * Object = (UObject*)Data; TArray< FRepChangedParent > LocalChangedParents; LocalChangedParents.SetNum( OtherChangedParents.Num() ); // Do an actual check to see which properties are different from our internal shadow state, and make sure the passed in change list matches const bool Result = CompareProperties( RepState, CompareData, Data, LocalChangedParents, PropertyList ); if ( Result != ChangedParentsHasChanged( PropertyList, OtherChangedParents ) ) { LogChangeListMismatches( Data, OwningChannel, PropertyList, RepState, OtherRepState, LocalChangedParents, OtherChangedParents ); UE_LOG( LogNet, Fatal, TEXT( "ReplicateProperties: Compare result mismatch: %s" ), *Object->GetName() ); } for ( int32 i = 0; i < PropertyList.Num(); i++ ) { const int32 Index = PropertyList[i]; if ( OtherChangedParents[Index].Changed.Num() != LocalChangedParents[Index].Changed.Num() ) { LogChangeListMismatches( Data, OwningChannel, PropertyList, RepState, OtherRepState, LocalChangedParents, OtherChangedParents ); UE_LOG( LogNet, Fatal, TEXT( "ReplicateProperties: Compare count mismatch: %s" ), *Object->GetName() ); } for ( int32 j = 0; j < OtherChangedParents[Index].Changed.Num(); j++ ) { if ( OtherChangedParents[Index].Changed[j] != LocalChangedParents[Index].Changed[j] ) { LogChangeListMismatches( Data, OwningChannel, PropertyList, RepState, OtherRepState, LocalChangedParents, OtherChangedParents ); UE_LOG( LogNet, Fatal, TEXT( "ReplicateProperties: Compare changelist value mismatch: %s" ), *Object->GetName() ); } } } } bool FRepLayout::ReplicateProperties( FRepState * RESTRICT RepState, const uint8 * RESTRICT Data, UClass * ObjectClass, UActorChannel * OwningChannel, FOutBunch & Writer, const FReplicationFlags & RepFlags, int32 & LastIndex, bool & bContentBlockWritten ) const { SCOPE_CYCLE_COUNTER( STAT_NetReplicateDynamicPropTime ); check( ObjectClass == Owner ); UObject * Object = (UObject*)Data; const UNetDriver * NetDriver = OwningChannel->Connection->Driver; FRepChangedPropertyTracker * ChangeTracker = RepState->RepChangedPropertyTracker.Get(); const uint8 * CompareData = RepState->StaticBuffer.GetTypedData(); // Rebuild conditional properties if needed if ( RepState->RepFlags.Value != RepFlags.Value || RepState->ActiveStatusChanged != ChangeTracker->ActiveStatusChanged ) { RebuildConditionalProperties( RepState, *ChangeTracker, RepFlags ); RepState->RepFlags.Value = RepFlags.Value; RepState->ActiveStatusChanged = ChangeTracker->ActiveStatusChanged; } bool PropertyChanged = false; #ifdef ENABLE_SUPER_CHECKSUMS const bool bIsAllAcked = AllAcked( RepState ); if ( bIsAllAcked || !RepState->OpenAckedCalled ) #endif { const int32 AllowSkipping = CVarAllowPropertySkipping.GetValueOnGameThread(); const bool bCanSkip = AllowSkipping > 0 && RepState->LastReplicationFrame != 0 && ChangeTracker->LastReplicationFrame == NetDriver->ReplicationFrame && ChangeTracker->LastReplicationGroupFrame == RepState->LastReplicationFrame; if ( bCanSkip ) { INC_DWORD_STAT_BY( STAT_NetSkippedDynamicProps, UnconditionalLifetime.Num() ); if ( AllowSkipping == 2 ) { // Sanity check results check( ChangeTracker->UnconditionalPropChanged == ChangedParentsHasChanged( UnconditionalLifetime, ChangeTracker->Parents ) ); SanityCheckShadowStateAgainstChangeList( RepState, Data, OwningChannel, UnconditionalLifetime, ChangeTracker->LastRepState, ChangeTracker->Parents ); } } else { // FRepState group changed, force this group to compare again this frame // This happens either once a frame, which is normal, or multiple times a frame // when multiple connections of the same actor aren't updated at the same time ChangeTracker->LastReplicationFrame = NetDriver->ReplicationFrame; ChangeTracker->LastReplicationGroupFrame = RepState->LastReplicationFrame; ChangeTracker->LastRepState = RepState; // Reset changed list if anything changed last time if ( ChangeTracker->UnconditionalPropChanged ) { for ( int32 i = UnconditionalLifetime.Num() - 1; i >= 0; i-- ) { ChangeTracker->Parents[UnconditionalLifetime[i]].Changed.Empty(); } } // Loop over all unconditional lifetime properties ChangeTracker->UnconditionalPropChanged = CompareProperties( RepState, CompareData, Data, ChangeTracker->Parents, UnconditionalLifetime ); } // Remember the last frame this FRepState was replicated, so we can note above when the FRepState replication group changes RepState->LastReplicationFrame = NetDriver->ReplicationFrame; if ( ChangeTracker->UnconditionalPropChanged ) { PropertyChanged = true; } // Loop over all the conditional properties if ( CompareProperties( RepState, CompareData, Data, ChangeTracker->Parents, RepState->ConditionalLifetime ) ) { PropertyChanged = true; } } #ifdef ENABLE_SUPER_CHECKSUMS else { // If we didn't compare this frame, make sure to reset out replication frame // This is to force a compare next time it comes up RepState->LastReplicationFrame = 0; } #endif // PreOpenAckHistory are all the properties sent before we got our first open ack const bool bFlushPreOpenAckHistory = RepState->OpenAckedCalled && RepState->PreOpenAckHistory.Num() > 0; if ( PropertyChanged || RepState->NumNaks > 0 || bFlushPreOpenAckHistory ) { // Use the first inactive history item to build this change list on check( RepState->HistoryEnd - RepState->HistoryStart < FRepState::MAX_CHANGE_HISTORY ); const int32 HistoryIndex = RepState->HistoryEnd % FRepState::MAX_CHANGE_HISTORY; FRepChangedHistory & NewHistoryItem = RepState->ChangeHistory[ HistoryIndex ]; RepState->HistoryEnd++; TArray & Changed = NewHistoryItem.Changed; check( Changed.Num() == 0 ); // Make sure this history item is actually inactive if ( PropertyChanged ) { // Initialize the history item change list with the parent change lists // We do it in the order of the parents so that the final change list will be fully sorted for ( int32 i = 0; i < Parents.Num(); i++ ) { if ( ChangeTracker->Parents[i].Changed.Num() > 0 ) { Changed.Append( ChangeTracker->Parents[i].Changed ); if ( Parents[i].Flags & PARENT_IsConditional ) { // Reset properties that don't share information across connections ChangeTracker->Parents[i].Changed.Empty(); } } } Changed.Add( 0 ); #ifdef SANITY_CHECK_MERGES SanityCheckChangeList( Data, Changed ); #endif } // Update the history, and merge in any nak'd change lists UpdateChangelistHistory( RepState, ObjectClass, Data, OwningChannel->Connection->OutAckPacketId, &Changed ); // Merge in the PreOpenAckHistory (unreliable properties sent before the bunch was initially acked) if ( bFlushPreOpenAckHistory ) { for ( int32 i = 0; i < RepState->PreOpenAckHistory.Num(); i++ ) { TArray< uint16 > Temp = Changed; Changed.Empty(); MergeDirtyList( (void*)Data, Temp, RepState->PreOpenAckHistory[i].Changed, Changed ); } RepState->PreOpenAckHistory.Empty(); } // At this point we should have a non empty change list check( Changed.Num() > 0 ); #ifdef SANITY_CHECK_MERGES SanityCheckChangeList( Data, Changed ); #endif // For RepLayout properties, we hijack the first non custom property, and use that to identify these properties WritePropertyHeader( (UObject*)Data, ObjectClass, OwningChannel, Parents[FirstNonCustomParent].Property, Writer, 0, LastIndex, bContentBlockWritten ); // Send the final merged change list SendProperties( RepState, RepFlags, Data, ObjectClass, OwningChannel, Writer, Changed, LastIndex, bContentBlockWritten ); #ifdef ENABLE_SUPER_CHECKSUMS Writer.WriteBit( bIsAllAcked ? 1 : 0 ); if ( bIsAllAcked ) { ValidateWithChecksum( RepState->StaticBuffer.GetTypedData(), Writer, false ); } #endif return true; } // Nothing changed and there are no nak's, so just do normal housekeeping and remove acked history items UpdateChangelistHistory( RepState, ObjectClass, Data, OwningChannel->Connection->OutAckPacketId, NULL ); return false; } void FRepLayout::UpdateChangelistHistory( FRepState * RepState, UClass * ObjectClass, const uint8 * RESTRICT Data, const int32 AckPacketId, TArray< uint16 > * OutMerged ) const { check( RepState->HistoryEnd >= RepState->HistoryStart ); const int32 HistoryCount = RepState->HistoryEnd - RepState->HistoryStart; const bool DumpHistory = HistoryCount == FRepState::MAX_CHANGE_HISTORY; // If our buffer is currently full, forcibly send the entire history if ( DumpHistory ) { UE_LOG( LogNet, Warning, TEXT( "FRepLayout::UpdateChangelistHistory: History overflow, forcing history dump %s" ), *ObjectClass->GetName() ); } for ( int32 i = RepState->HistoryStart; i < RepState->HistoryEnd; i++ ) { const int32 HistoryIndex = i % FRepState::MAX_CHANGE_HISTORY; FRepChangedHistory & HistoryItem = RepState->ChangeHistory[ HistoryIndex ]; if ( HistoryItem.OutPacketIdRange.First == INDEX_NONE ) { continue; // Hasn't been initialized in PostReplicate yet } check( HistoryItem.Changed.Num() > 0 ); // All active history items should contain a change list if ( AckPacketId >= HistoryItem.OutPacketIdRange.Last || HistoryItem.Resend || DumpHistory ) { if ( HistoryItem.Resend || DumpHistory ) { // Merge in nak'd change lists check( OutMerged != NULL ); TArray< uint16 > Temp = *OutMerged; OutMerged->Empty(); MergeDirtyList( (void*)Data, Temp, HistoryItem.Changed, *OutMerged ); HistoryItem.Changed.Empty(); #ifdef SANITY_CHECK_MERGES SanityCheckChangeList( Data, *OutMerged ); #endif if ( HistoryItem.Resend ) { HistoryItem.Resend = false; RepState->NumNaks--; } } HistoryItem.Changed.Empty(); HistoryItem.OutPacketIdRange = FPacketIdRange(); RepState->HistoryStart++; } } // Remove any tiling in the history markers to keep them from wrapping over time const int32 NewHistoryCount = RepState->HistoryEnd - RepState->HistoryStart; RepState->HistoryStart = RepState->HistoryStart % FRepState::MAX_CHANGE_HISTORY; RepState->HistoryEnd = RepState->HistoryStart + NewHistoryCount; check( RepState->NumNaks == 0 ); // Make sure we processed all the naks properly } void FRepLayout::OpenAcked( FRepState * RepState ) const { check( RepState != NULL ); RepState->OpenAckedCalled = true; } void FRepLayout::PostReplicate( FRepState * RepState, FPacketIdRange & PacketRange, bool bReliable ) const { for ( int32 i = RepState->HistoryStart; i < RepState->HistoryEnd; i++ ) { const int32 HistoryIndex = i % FRepState::MAX_CHANGE_HISTORY; FRepChangedHistory & HistoryItem = RepState->ChangeHistory[ HistoryIndex ]; if ( HistoryItem.OutPacketIdRange.First == INDEX_NONE ) { check( HistoryItem.Changed.Num() > 0 ); check( !HistoryItem.Resend ); HistoryItem.OutPacketIdRange = PacketRange; if ( !bReliable && !RepState->OpenAckedCalled ) { RepState->PreOpenAckHistory.Add( HistoryItem ); } } } } void FRepLayout::ReceivedNak( FRepState * RepState, int32 NakPacketId ) const { if ( RepState == NULL ) { return; // I'm not 100% certain why this happens, the only think I can think of is this is a bNetTemporary? } for ( int32 i = RepState->HistoryStart; i < RepState->HistoryEnd; i++ ) { const int32 HistoryIndex = i % FRepState::MAX_CHANGE_HISTORY; FRepChangedHistory & HistoryItem = RepState->ChangeHistory[ HistoryIndex ]; if ( !HistoryItem.Resend && HistoryItem.OutPacketIdRange.InRange( NakPacketId ) ) { check( HistoryItem.Changed.Num() > 0 ); HistoryItem.Resend = true; RepState->NumNaks++; } } } bool FRepLayout::AllAcked( FRepState * RepState ) const { if ( RepState->HistoryStart != RepState->HistoryEnd ) { // We have change lists that haven't been acked return false; } if ( RepState->NumNaks > 0 ) { return false; } if ( !RepState->OpenAckedCalled ) { return false; } if ( RepState->PreOpenAckHistory.Num() > 0 ) { return false; } return true; } bool FRepLayout::ReadyForDormancy( FRepState * RepState ) const { if ( RepState == NULL ) { return false; } return AllAcked( RepState ); } static FORCEINLINE void WritePropertyHandle( FNetBitWriter & Writer, uint16 Handle, bool bDoChecksum ) { uint32 LocalHandle = Handle; Writer.SerializeIntPacked( LocalHandle ); #ifdef ENABLE_PROPERTY_CHECKSUMS if ( bDoChecksum ) { SerializeGenericChecksum( Writer ); } #endif } static bool ShouldSendProperty( FRepWriterState & WriterState, const uint16 Handle ) { if ( Handle == WriterState.Changed[WriterState.CurrentChanged] ) { // Write out the handle WritePropertyHandle( WriterState.Writer, Handle, WriterState.bDoChecksum ); // Advance to the next expected handle WriterState.CurrentChanged++; return true; } return false; } void FRepLayout::SendProperties_DynamicArray_r( FRepState * RESTRICT RepState, const FReplicationFlags & RepFlags, FRepWriterState & WriterState, const int32 CmdIndex, const uint8 * RESTRICT StoredData, const uint8 * RESTRICT Data, uint16 Handle ) const { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; FScriptArray * Array = (FScriptArray *)Data; FScriptArray * StoredArray = (FScriptArray *)StoredData; // Write array num uint16 ArrayNum = Array->Num(); WriterState.Writer << ArrayNum; // Make the shadow state match the actual state at the time of send FScriptArrayHelper StoredArrayHelper( (UArrayProperty *)Cmd.Property, StoredData ); StoredArrayHelper.Resize( ArrayNum ); // Read the jump offset // We won't need to actually jump over anything because we expect the change list to be pruned once we get here // But we can use it to verify we read the correct amount. const int32 ArrayChangedCount = WriterState.Changed[WriterState.CurrentChanged++]; const int32 OldChangedIndex = WriterState.CurrentChanged; Data = (uint8*)Array->GetData(); StoredData = (uint8*)StoredArray->GetData(); uint16 LocalHandle = 0; for ( int32 i = 0; i < Array->Num(); i++ ) { const int32 ElementOffset = i * Cmd.ElementSize; LocalHandle = SendProperties_r( RepState, RepFlags, WriterState, CmdIndex + 1, Cmd.EndCmd - 1, StoredData + ElementOffset, Data + ElementOffset, LocalHandle ); } check( WriterState.CurrentChanged - OldChangedIndex == ArrayChangedCount ); // Make sure we read correct amount check( WriterState.Changed[WriterState.CurrentChanged] == 0 ); // Make sure we are at the end WriterState.CurrentChanged++; WritePropertyHandle( WriterState.Writer, 0, WriterState.bDoChecksum ); // Signify end of dynamic array } uint16 FRepLayout::SendProperties_r( FRepState * RESTRICT RepState, const FReplicationFlags & RepFlags, FRepWriterState & WriterState, const int32 CmdStart, const int32 CmdEnd, const uint8 * RESTRICT StoredData, const uint8 * RESTRICT Data, uint16 Handle ) const { for ( int32 CmdIndex = CmdStart; CmdIndex < CmdEnd; CmdIndex++ ) { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; check( Cmd.Type != REPCMD_Return ); Handle++; if ( Cmd.Type == REPCMD_DynamicArray ) { if ( ShouldSendProperty( WriterState, Handle ) ) { SendProperties_DynamicArray_r( RepState, RepFlags, WriterState, CmdIndex, StoredData + Cmd.Offset, Data + Cmd.Offset, Handle ); } CmdIndex = Cmd.EndCmd - 1; // Jump past children of this array (-1 for the ++ in the for loop) continue; } if ( ShouldSendProperty( WriterState, Handle ) ) { #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) if (CVarDoReplicationContextString->GetInt() > 0) { WriterState.Writer.PackageMap->SetDebugContextString( FString::Printf(TEXT("%s - %s"), *Owner->GetPathName(), *Cmd.Property->GetPathName() ) ); } #endif const int32 NumStartBits = WriterState.Writer.GetNumBits(); // This property changed, so send it Cmd.Property->NetSerializeItem( WriterState.Writer, WriterState.Writer.PackageMap, (void*)( Data + Cmd.Offset ) ); const int32 NumEndBits = WriterState.Writer.GetNumBits(); const FRepParentCmd & ParentCmd = Parents[Cmd.ParentIndex]; NETWORK_PROFILER( GNetworkProfiler.TrackReplicateProperty( ParentCmd.Property, false, false, 0, 0, NumEndBits - NumStartBits ) ); // Make the shadow state match the actual state at the time of send StoreProperty( Cmd, (void*)( StoredData + Cmd.Offset ), (const void*)( Data + Cmd.Offset ) ); #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) if (CVarDoReplicationContextString->GetInt() > 0) { WriterState.Writer.PackageMap->ClearDebugContextString(); } #endif #ifdef ENABLE_PROPERTY_CHECKSUMS if ( WriterState.bDoChecksum ) { SerializeReadWritePropertyChecksum( Cmd, CmdIndex, Data + Cmd.Offset, WriterState.Writer, false ); } #endif } } return Handle; } void FRepLayout::WritePropertyHeader( UObject * Object, UClass * ObjectClass, UActorChannel * OwningChannel, UProperty * Property, FOutBunch & Bunch, int32 ArrayIndex, int32 & LastArrayIndex, bool & bContentBlockWritten ) const { UNetConnection * Connection = OwningChannel->Connection; // Get class network info cache. FClassNetCache * ClassCache = Connection->Driver->NetCache->GetClassNetCache( ObjectClass ); check( ClassCache ); if ( !bContentBlockWritten ) { OwningChannel->BeginContentBlock( Object, Bunch ); bContentBlockWritten = true; } // Get the network friend property index to replicate // Swap Role/RemoteRole while we're at it FFieldNetCache * FieldCache = Property->GetFName() == NAME_Role ? ClassCache->GetFromField( Connection->Driver->RemoteRoleProperty ) : Property->GetFName() == NAME_RemoteRole ? ClassCache->GetFromField( Connection->Driver->RoleProperty ) : ClassCache->GetFromField( Property ); checkSlow( FieldCache ); // Send property name and optional array index. check( FieldCache->FieldNetIndex <= ClassCache->GetMaxIndex() ); Bunch.WriteIntWrapped( FieldCache->FieldNetIndex, ClassCache->GetMaxIndex() + 1 ); NET_CHECKSUM( Bunch ); if ( Property->ArrayDim != 1 ) { // Serialize index as delta from previous index to increase chance we'll only use 1 byte uint32 idx = static_cast( ArrayIndex - LastArrayIndex ); Bunch.SerializeIntPacked(idx); LastArrayIndex = ArrayIndex; } } void FRepLayout::SendProperties( FRepState * RESTRICT RepState, const FReplicationFlags & RepFlags, const uint8 * RESTRICT Data, UClass * ObjectClass, UActorChannel * OwningChannel, FOutBunch & Writer, TArray< uint16 > & Changed, int32 & LastIndex, bool & bContentBlockWritten ) const { #ifdef ENABLE_PROPERTY_CHECKSUMS const bool bDoChecksum = CVarDoPropertyChecksum.GetValueOnGameThread() == 1; #else const bool bDoChecksum = false; #endif FRepWriterState WriterState( Writer, Changed, bDoChecksum ); #ifdef ENABLE_PROPERTY_CHECKSUMS Writer.WriteBit( bDoChecksum ? 1 : 0 ); #endif SendProperties_r( RepState, RepFlags, WriterState, 0, Cmds.Num() - 1, RepState->StaticBuffer.GetTypedData(), Data, 0 ); WritePropertyHandle( Writer, 0, bDoChecksum ); } static void ReadNextHandle( FRepReaderState & ReaderState ) { ReaderState.Bunch.SerializeIntPacked( ReaderState.WaitingHandle ); #ifdef ENABLE_PROPERTY_CHECKSUMS if ( ReaderState.bDoChecksum ) { SerializeGenericChecksum( ReaderState.Bunch ); } #endif } static bool ShouldReadProperty( FRepReaderState & ReaderState ) { ReaderState.CurrentHandle++; if ( ReaderState.CurrentHandle == ReaderState.WaitingHandle ) { check( ReaderState.WaitingHandle != 0 ); return true; } return false; } bool FRepLayout::ReadProperty( FRepReaderState & ReaderState, FUnmappedGuidMgr * UnmappedGuids, const int32 AbsOffset, const FRepLayoutCmd & Cmd, const int32 CmdIndex, uint8 * RESTRICT StoredData, uint8 * RESTRICT Data, const bool bDiscard ) const { if ( !ShouldReadProperty( ReaderState ) ) { // This property didn't change, nothing to read return true; } if ( bDiscard ) { FMemMark Mark( FMemStack::Get() ); uint8 * DiscardBuffer = NewZeroed( FMemStack::Get(), Cmd.ElementSize ); const int32 OldArrayDim = Cmd.Property->ArrayDim; Cmd.Property->ArrayDim = 1; // Force this to 1, so that InitializeValue/DestroyValue work on a single item Cmd.Property->InitializeValue( DiscardBuffer ); Cmd.Property->NetSerializeItem( ReaderState.Bunch, ReaderState.Bunch.PackageMap, DiscardBuffer ); Cmd.Property->DestroyValue( DiscardBuffer ); Cmd.Property->ArrayDim = OldArrayDim; Mark.Pop(); // Read the next property handle ReadNextHandle( ReaderState ); return true; } const FRepParentCmd & Parent = Parents[Cmd.ParentIndex]; // This swaps Role/RemoteRole as we write it const FRepLayoutCmd & SwappedCmd = Parent.RoleSwapIndex != -1 ? Cmds[Parents[Parent.RoleSwapIndex].CmdStart] : Cmd; ReaderState.Bunch.PackageMap->ResetLoadedUnmappedObject(); if ( Parent.Property->HasAnyPropertyFlags( CPF_RepNotify ) ) { // Copy current value over so we can check to see if it changed StoreProperty( Cmd, StoredData + Cmd.Offset, Data + SwappedCmd.Offset ); // Read the property Cmd.Property->NetSerializeItem( ReaderState.Bunch, ReaderState.Bunch.PackageMap, Data + SwappedCmd.Offset ); // Check to see if this property changed if ( !PropertiesAreIdentical( Cmd, StoredData + Cmd.Offset, Data + SwappedCmd.Offset ) ) { ReaderState.RepState->RepNotifies.AddUnique( Parent.Property ); } } else { Cmd.Property->NetSerializeItem( ReaderState.Bunch, ReaderState.Bunch.PackageMap, Data + SwappedCmd.Offset ); } if ( ReaderState.Bunch.PackageMap->GetLoadedUnmappedObject() ) { // If we have an unmapped guid, we need to remember it, so we can fix up this object pointer when it finally arrives at a other time const int32 LocalAbsOffset = AbsOffset + SwappedCmd.Offset; UnmappedGuids->Map.Add( LocalAbsOffset, FUnmappedGuidMgrElement( ReaderState.Bunch.PackageMap->GetLastUnmappedNetGUID(), Cmd.ParentIndex, CmdIndex ) ); UE_LOG( LogNet, Verbose, TEXT( "ADDED unmapped property: Offset: %i, Guid: %s, Name: %s"), LocalAbsOffset, *ReaderState.Bunch.PackageMap->GetLastUnmappedNetGUID().ToString(), *Cmd.Property->GetName() ); ReaderState.bHasUnmapped = true; } #ifdef ENABLE_PROPERTY_CHECKSUMS if ( ReaderState.bDoChecksum ) { SerializeReadWritePropertyChecksum( Cmd, CmdIndex, Data + SwappedCmd.Offset, ReaderState.Bunch, false ); } #endif // Store the property we just received FIXME: Can't do this because of rep notifies. This needs to be done for super checksums though, FIXME!!! //StoreProperty( Cmd, StoredData + Cmd.Offset, Data + SwappedCmd.Offset ); // Read the next property handle ReadNextHandle( ReaderState ); return true; } bool FRepLayout::ReceiveProperties_AnyArray_r( FRepReaderState & ReaderState, FUnmappedGuidMgr * UnmappedGuids, const int32 AbsOffset, const int32 ArrayNum, const int32 ElementSize, const int32 CmdIndex, uint8 * RESTRICT StoredData, uint8 * RESTRICT Data, const bool bDiscard ) const { const int32 CmdStart = CmdIndex + 1; const int32 CmdEnd = Cmds[CmdIndex].EndCmd - 1; for ( int32 i = 0; i < ArrayNum; i++ ) { const int32 ElementOffset = i * ElementSize; if ( !ReceiveProperties_r( ReaderState, UnmappedGuids, AbsOffset + ElementOffset, CmdStart, CmdEnd, StoredData + ElementOffset, Data + ElementOffset, bDiscard ) ) { return false; } } return true; } bool FRepLayout::ReceiveProperties_DynamicArray_r( FRepReaderState & ReaderState, FUnmappedGuidMgr * UnmappedGuids, const int32 AbsOffset, const FRepLayoutCmd & Cmd, const int32 CmdIndex, uint8 * RESTRICT StoredData, uint8 * RESTRICT Data, const bool bDiscard ) const { if ( !ShouldReadProperty( ReaderState ) ) { // This array didn't change, nothing to read return true; } // Read array size uint16 ArrayNum = 0; ReaderState.Bunch << ArrayNum; // Read the next property handle ReadNextHandle( ReaderState ); if ( !bDiscard ) { FScriptArray * Array = (FScriptArray *)Data; FScriptArray * StoredArray = (FScriptArray *)StoredData; // Since we don't know yet if something under us could be unmapped, go ahead and allocate an array container now FUnmappedGuidMgrElement * NewArrayElement = UnmappedGuids->Map.Find( AbsOffset ); if ( NewArrayElement == NULL ) { NewArrayElement = &UnmappedGuids->Map.FindOrAdd( AbsOffset ); NewArrayElement->Array = new FUnmappedGuidMgr; NewArrayElement->ParentIndex = Cmd.ParentIndex; NewArrayElement->CmdIndex = CmdIndex; } check( NewArrayElement != NULL ); check( NewArrayElement->ParentIndex == Cmd.ParentIndex ); check( NewArrayElement->CmdIndex == CmdIndex ); UnmappedGuids = NewArrayElement->Array; const FRepParentCmd & Parent = Parents[Cmd.ParentIndex]; if ( Array->Num() != ArrayNum && Parent.Property->HasAnyPropertyFlags( CPF_RepNotify ) ) { ReaderState.RepState->RepNotifies.AddUnique( Parent.Property ); } // Set the array to the correct size (if we aren't discarding) FScriptArrayHelper ArrayHelper( (UArrayProperty *)Cmd.Property, Data ); ArrayHelper.Resize( ArrayNum ); FScriptArrayHelper StoredArrayHelper( (UArrayProperty *)Cmd.Property, StoredData ); StoredArrayHelper.Resize( ArrayNum ); Data = (uint8*)Array->GetData(); StoredData = (uint8*)StoredArray->GetData(); } const uint16 OldHandle = ReaderState.CurrentHandle; // Array children handles are always relative to their immediate parent ReaderState.CurrentHandle = 0; // Read any changed properties into the array if ( !ReceiveProperties_AnyArray_r( ReaderState, UnmappedGuids, 0, ArrayNum, Cmd.ElementSize, CmdIndex, StoredData, Data, bDiscard ) ) { return false; } ReaderState.CurrentHandle = OldHandle; // We should be waiting on the NULL terminator handle at this point check( ReaderState.WaitingHandle == 0 ); ReadNextHandle( ReaderState ); return true; } bool FRepLayout::ReceiveProperties_r( FRepReaderState & ReaderState, FUnmappedGuidMgr * UnmappedGuids, const int32 AbsOffset, const int32 CmdStart, const int32 CmdEnd, uint8 * RESTRICT StoredData, uint8 * RESTRICT Data, const bool bDiscard ) const { for ( int32 CmdIndex = CmdStart; CmdIndex < CmdEnd; CmdIndex++ ) { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; check( Cmd.Type != REPCMD_Return ); if ( Cmd.Type == REPCMD_DynamicArray ) { if ( !ReceiveProperties_DynamicArray_r( ReaderState, UnmappedGuids, AbsOffset + Cmd.Offset, Cmd, CmdIndex, StoredData + Cmd.Offset, Data + Cmd.Offset, bDiscard ) ) { return false; } CmdIndex = Cmd.EndCmd - 1; // Jump past children of this array ( -1 for the ++ in the for loop) continue; } if ( !ReadProperty( ReaderState, UnmappedGuids, AbsOffset, Cmd, CmdIndex, StoredData, Data, bDiscard ) ) { return false; } } return true; } bool FRepLayout::ReceiveProperties( UClass * InObjectClass, FRepState * RESTRICT RepState, void * RESTRICT Data, FNetBitReader & InBunch, bool bDiscard, bool & bOutHasUnmapped ) const { check( InObjectClass == Owner ); #ifdef ENABLE_PROPERTY_CHECKSUMS const bool bDoChecksum = InBunch.ReadBit() ? true : false; #else const bool bDoChecksum = false; #endif bOutHasUnmapped = false; FRepReaderState ReaderState( InBunch, RepState, bDoChecksum ); // Read first handle ReadNextHandle( ReaderState ); // Read all properties if ( !ReceiveProperties_r( ReaderState, &RepState->UnmappedGuids, 0, 0, Cmds.Num() - 1, RepState->StaticBuffer.GetTypedData(), (uint8*)Data, bDiscard ) ) { return false; } // Make sure we're waiting on the last NULL terminator if ( ReaderState.WaitingHandle != 0 ) { UE_LOG( LogNet, Warning, TEXT( "Read out of sync." ) ); return false; } #ifdef ENABLE_SUPER_CHECKSUMS if ( InBunch.ReadBit() == 1 ) { ValidateWithChecksum( RepState->StaticBuffer.GetTypedData(), InBunch, bDiscard ); } #endif bOutHasUnmapped = ReaderState.bHasUnmapped; return true; } FUnmappedGuidMgrElement::~FUnmappedGuidMgrElement() { if ( Array != NULL ) { delete Array; Array = NULL; } } void FRepLayout::UpdateUnmappedObjects_r( FRepState * RepState, FUnmappedGuidMgr * UnmappedGuids, UObject * OriginalObject, UPackageMap * PackageMap, uint8 * RESTRICT Data, const int32 MaxAbsOffset, bool & bOutSomeObjectsWereMapped, bool & bOutHasMoreUnmapped ) const { bool bHasUnmapped = false; for ( auto It = UnmappedGuids->Map.CreateIterator(); It; ++It ) { const int32 AbsOffset = It.Key(); if ( AbsOffset >= MaxAbsOffset ) { // Array must have shrunk, we can remove this item UE_LOG( LogNet, VeryVerbose, TEXT( "REMOVED unmapped property: AbsOffset >= MaxAbsOffset. Offset: %i" ), AbsOffset ); It.RemoveCurrent(); continue; } const FRepLayoutCmd & Cmd = Cmds[It.Value().CmdIndex]; const FRepParentCmd & Parent = Parents[It.Value().ParentIndex]; if ( It.Value().Array != NULL ) { check( Cmd.Type == REPCMD_DynamicArray ); FScriptArray * Array = (FScriptArray *)( Data + AbsOffset ); UpdateUnmappedObjects_r( RepState, It.Value().Array, OriginalObject, PackageMap, (uint8*)Array->GetData(), Array->Num() * Cmd.ElementSize, bOutSomeObjectsWereMapped, bOutHasMoreUnmapped ); continue; } check( Cmd.Type == REPCMD_PropertyObject ); UObject * Object = PackageMap->GetObjectFromNetGUID( It.Value().Guid ); if ( Object != NULL ) { UE_LOG( LogNet, VeryVerbose, TEXT( "REMOVED unmapped property: Offset: %i, Guid: %s, PropName: %s, ObjName: %s" ), AbsOffset, *It.Value().Guid.ToString(), *Cmd.Property->GetName(), *Object->GetName() ); UObjectPropertyBase * ObjProperty = CastChecked< UObjectPropertyBase>( Cmd.Property ); UObject * OldObject = ObjProperty->GetObjectPropertyValue( Data + AbsOffset ); if ( OldObject != Object ) { if ( !bOutSomeObjectsWereMapped ) { // Call PreNetReceive if we are going to change a value (some game code will need to think this is an actual replicated value) OriginalObject->PreNetReceive(); bOutSomeObjectsWereMapped = true; } ObjProperty->SetObjectPropertyValue( Data + AbsOffset, Object ); // If this properties needs an OnRep, queue that up to be handled later if ( Parent.Property->HasAnyPropertyFlags( CPF_RepNotify ) ) { RepState->RepNotifies.AddUnique( Parent.Property ); } } It.RemoveCurrent(); continue; } UE_LOG( LogNet, VeryVerbose, TEXT( "UnmappedGuids: Offset: %i, Guid: %s" ), AbsOffset, *It.Value().Guid.ToString() ); bOutHasMoreUnmapped = true; } } void FRepLayout::UpdateUnmappedObjects( FRepState * RepState, UPackageMap * PackageMap, UObject * OriginalObject, bool & bOutSomeObjectsWereMapped, bool & bOutHasMoreUnmapped ) const { bOutSomeObjectsWereMapped = false; bOutHasMoreUnmapped = false; UpdateUnmappedObjects_r( RepState, &RepState->UnmappedGuids, OriginalObject, PackageMap, (uint8*)OriginalObject, RepState->StaticBuffer.Num(), bOutSomeObjectsWereMapped, bOutHasMoreUnmapped ); } void FRepLayout::CallRepNotifies( FRepState * RepState, UObject * Object ) const { if ( RepState->RepNotifies.Num() == 0 ) { return; } for ( int32 i = 0; i < RepState->RepNotifies.Num(); i++ ) { UProperty * RepProperty = RepState->RepNotifies[i]; UFunction * RepNotifyFunc = Object->FindFunctionChecked( RepProperty->RepNotifyFunc ); check( RepNotifyFunc->NumParms <= 1 ); // 2 parms not supported yet if ( RepNotifyFunc->NumParms == 0 ) { Object->ProcessEvent( RepNotifyFunc, NULL ); } else if (RepNotifyFunc->NumParms == 1 ) { Object->ProcessEvent( RepNotifyFunc, RepProperty->ContainerPtrToValuePtr( RepState->StaticBuffer.GetTypedData() ) ); } // Store the property we just received //StoreProperty( Cmd, StoredData + Cmd.Offset, Data + SwappedCmd.Offset ); } RepState->RepNotifies.Empty(); } void FRepLayout::ValidateWithChecksum_DynamicArray_r( const FRepLayoutCmd & Cmd, const int32 CmdIndex, const uint8 * RESTRICT Data, FArchive & Ar, const bool bDiscard ) const { if ( bDiscard ) { uint16 ArrayNum = 0; uint16 ElementSize = 0; Ar << ArrayNum; Ar << ElementSize; for ( int32 i = 0; i < ArrayNum; i++ ) { ValidateWithChecksum_r( CmdIndex + 1, Cmd.EndCmd - 1, NULL, Ar, bDiscard ); } return; } FScriptArray * Array = (FScriptArray *)Data; uint16 ArrayNum = Array->Num(); uint16 ElementSize = Cmd.ElementSize; Ar << ArrayNum; Ar << ElementSize; if ( ArrayNum != Array->Num() ) { UE_LOG( LogNet, Fatal, TEXT( "ValidateWithChecksum_AnyArray_r: Array sizes different! %s %i / %i" ), *Cmd.Property->GetFullName(), ArrayNum, Array->Num() ); } if ( ElementSize != Cmd.ElementSize ) { UE_LOG( LogNet, Fatal, TEXT( "ValidateWithChecksum_AnyArray_r: Array element sizes different! %s %i / %i" ), *Cmd.Property->GetFullName(), ElementSize, Cmd.ElementSize ); } uint8 * LocalData = (uint8*)Array->GetData(); for ( int32 i = 0; i < ArrayNum; i++ ) { ValidateWithChecksum_r( CmdIndex + 1, Cmd.EndCmd - 1, LocalData + i * ElementSize, Ar, bDiscard ); } } void FRepLayout::ValidateWithChecksum_r( const int32 CmdStart, const int32 CmdEnd, const uint8 * RESTRICT Data, FArchive & Ar, const bool bDiscard ) const { for ( int32 CmdIndex = CmdStart; CmdIndex < CmdEnd; CmdIndex++ ) { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; check( Cmd.Type != REPCMD_Return ); if ( Cmd.Type == REPCMD_DynamicArray ) { ValidateWithChecksum_DynamicArray_r( Cmd, CmdIndex, Data + Cmd.Offset, Ar, bDiscard ); CmdIndex = Cmd.EndCmd - 1; // Jump past children of this array (-1 for ++ in for loop) continue; } SerializeReadWritePropertyChecksum( Cmd, CmdIndex - 1, Data + Cmd.Offset, Ar, bDiscard ); } } void FRepLayout::ValidateWithChecksum( const void * RESTRICT Data, FArchive & Ar, const bool bDiscard ) const { ValidateWithChecksum_r( 0, Cmds.Num() - 1, (const uint8*)Data, Ar, bDiscard ); } uint32 FRepLayout::GenerateChecksum( const FRepState * RepState ) const { FBitWriter Writer( 1024, true ); ValidateWithChecksum_r( 0, Cmds.Num() - 1, (const uint8*)RepState->StaticBuffer.GetTypedData(), Writer, false ); return FCrc::MemCrc32( Writer.GetData(), Writer.GetNumBytes(), 0 ); } void FRepLayout::MergeDirtyList_AnyArray_r( FMergeDirtyListState & MergeState, const FRepLayoutCmd & Cmd, const int32 ArrayNum, const int32 ElementSize, const int32 CmdIndex, const uint8 * RESTRICT Data ) const { for ( int32 i = 0; i < ArrayNum; i++ ) { MergeDirtyList_r( MergeState, CmdIndex + 1, Cmd.EndCmd - 1, Data + i * ElementSize ); } } void FRepLayout::MergeDirtyList_DynamicArray_r( FMergeDirtyListState & MergeState, const FRepLayoutCmd & Cmd, const int32 CmdIndex, const uint8 * RESTRICT Data ) const { // At least one of the list should be valid to be here check( MergeState.DirtyValid1 || MergeState.DirtyValid2 ); MergeState.Handle++; const bool Dirty1Matches = MergeState.DirtyValid1 && MergeState.DirtyList1[MergeState.DirtyListIndex1] == MergeState.Handle; const bool Dirty2Matches = MergeState.DirtyValid2 && MergeState.DirtyList2[MergeState.DirtyListIndex2] == MergeState.Handle; if ( !Dirty1Matches && !Dirty2Matches ) { // Neither match, we're done with this branch return; } // This will be a new merged dirty entry MergeState.MergedDirtyList.Add( MergeState.Handle ); const int32 RememberIndex = MergeState.MergedDirtyList.AddUninitialized(); const int32 OldMergedListSize = MergeState.MergedDirtyList.Num(); // Advance the matching dirty lists if ( Dirty1Matches ) { MergeState.DirtyListIndex1++; } if ( Dirty2Matches ) { MergeState.DirtyListIndex2++; } // Remember valid dirty lists const bool OldDirtyValid1 = MergeState.DirtyValid1; const bool OldDirtyValid2 = MergeState.DirtyValid2; // Update which lists are still valid from this point MergeState.DirtyValid1 = Dirty1Matches; MergeState.DirtyValid2 = Dirty2Matches; const int32 JumpToIndex1 = Dirty1Matches ? MergeState.DirtyList1[MergeState.DirtyListIndex1++] : -1; const int32 JumpToIndex2 = Dirty2Matches ? MergeState.DirtyList2[MergeState.DirtyListIndex2++] : -1; const int32 OldDirtyListIndex1 = MergeState.DirtyListIndex1; const int32 OldDirtyListIndex2 = MergeState.DirtyListIndex2; FScriptArray * Array = (FScriptArray *)Data; const int32 OldHandle = MergeState.Handle; MergeState.Handle = 0; MergeDirtyList_AnyArray_r( MergeState, Cmd, Array->Num(), Cmd.ElementSize, CmdIndex, (uint8*)Array->GetData() ); MergeState.Handle = OldHandle; if ( Dirty1Matches ) { check( MergeState.DirtyListIndex1 - OldDirtyListIndex1 <= JumpToIndex1 ); MergeState.DirtyListIndex1 = OldDirtyListIndex1 + JumpToIndex1; check( MergeState.DirtyList1[MergeState.DirtyListIndex1] == 0 ); MergeState.DirtyListIndex1++; } if ( Dirty2Matches ) { check( MergeState.DirtyListIndex2 - OldDirtyListIndex2 <= JumpToIndex2 ); MergeState.DirtyListIndex2 = OldDirtyListIndex2 + JumpToIndex2; check( MergeState.DirtyList2[MergeState.DirtyListIndex2] == 0 ); MergeState.DirtyListIndex2++; } MergeState.DirtyValid1 = OldDirtyValid1; MergeState.DirtyValid2 = OldDirtyValid2; // Patch in the jump offset MergeState.MergedDirtyList[RememberIndex] = MergeState.MergedDirtyList.Num() - OldMergedListSize; // Add the array terminator MergeState.MergedDirtyList.Add( 0 ); } void FRepLayout::MergeDirtyList_r( FMergeDirtyListState & MergeState, const int32 CmdStart, const int32 CmdEnd, const uint8 * RESTRICT Data ) const { for ( int32 CmdIndex = CmdStart; CmdIndex < CmdEnd; CmdIndex++ ) { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; check( Cmd.Type != REPCMD_Return ); if ( Cmd.Type == REPCMD_DynamicArray ) { MergeDirtyList_DynamicArray_r( MergeState, Cmd, CmdIndex, Data + Cmd.Offset ); CmdIndex = Cmd.EndCmd - 1; // Jump past children of this array (-1 for ++ in for loop) continue; } MergeState.Handle++; const bool Dirty1Matches = MergeState.DirtyValid1 && MergeState.DirtyList1[MergeState.DirtyListIndex1] == MergeState.Handle; const bool Dirty2Matches = MergeState.DirtyValid2 && MergeState.DirtyList2[MergeState.DirtyListIndex2] == MergeState.Handle; if ( Dirty1Matches || Dirty2Matches ) { // This will be a new merged dirty entry MergeState.MergedDirtyList.Add( MergeState.Handle ); // Advance matching dirty indices if ( Dirty1Matches ) { MergeState.DirtyListIndex1++; } if ( Dirty2Matches ) { MergeState.DirtyListIndex2++; } } } } void FRepLayout::MergeDirtyList( const void * RESTRICT Data, const TArray< uint16 > & Dirty1, const TArray< uint16 > & Dirty2, TArray< uint16 > & MergedDirty ) const { check( Dirty1.Num() > 0 || Dirty2.Num() > 0 ); FMergeDirtyListState MergeState( Dirty1, Dirty2, MergedDirty ); MergedDirty.Empty(); // Even though one of these can be empty, we need to send the single one through, so we can prune it to the current shape of the tree MergeState.DirtyValid1 = Dirty1.Num() > 0; MergeState.DirtyValid2 = Dirty2.Num() > 0; MergeDirtyList_r( MergeState, 0, Cmds.Num() - 1, (const uint8*)Data ); MergeState.MergedDirtyList.Add( 0 ); } void FRepLayout::SanityCheckChangeList_DynamicArray_r( const int32 CmdIndex, const uint8 * RESTRICT Data, TArray< uint16 > & Changed, int32 & ChangedIndex ) const { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; FScriptArray * Array = (FScriptArray *)Data; // Read the jump offset // We won't need to actually jump over anything because we expect the change list to be pruned once we get here // But we can use it to verify we read the correct amount. const int32 ArrayChangedCount = Changed[ChangedIndex++]; const int32 OldChangedIndex = ChangedIndex; Data = (uint8*)Array->GetData(); uint16 LocalHandle = 0; for ( int32 i = 0; i < Array->Num(); i++ ) { LocalHandle = SanityCheckChangeList_r( CmdIndex + 1, Cmd.EndCmd - 1, Data + i * Cmd.ElementSize, Changed, ChangedIndex, LocalHandle ); } check( ChangedIndex - OldChangedIndex == ArrayChangedCount ); // Make sure we read correct amount check( Changed[ChangedIndex] == 0 ); // Make sure we are at the end ChangedIndex++; } uint16 FRepLayout::SanityCheckChangeList_r( const int32 CmdStart, const int32 CmdEnd, const uint8 * RESTRICT Data, TArray< uint16 > & Changed, int32 & ChangedIndex, uint16 Handle ) const { for ( int32 CmdIndex = CmdStart; CmdIndex < CmdEnd; CmdIndex++ ) { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; check( Cmd.Type != REPCMD_Return ); Handle++; if ( Cmd.Type == REPCMD_DynamicArray ) { if ( Handle == Changed[ChangedIndex] ) { const int32 LastChangedArrayHandle = Changed[ChangedIndex]; ChangedIndex++; SanityCheckChangeList_DynamicArray_r( CmdIndex, Data + Cmd.Offset, Changed, ChangedIndex ); check( Changed[ChangedIndex] == 0 || Changed[ChangedIndex] > LastChangedArrayHandle ); } CmdIndex = Cmd.EndCmd - 1; // Jump past children of this array (the -1 because of the ++ in the for loop) continue; } if ( Handle == Changed[ChangedIndex] ) { const int32 LastChangedArrayHandle = Changed[ChangedIndex]; ChangedIndex++; check( Changed[ChangedIndex] == 0 || Changed[ChangedIndex] > LastChangedArrayHandle ); } } return Handle; } void FRepLayout::SanityCheckChangeList( const uint8 * RESTRICT Data, TArray< uint16 > & Changed ) const { int32 ChangedIndex = 0; SanityCheckChangeList_r( 0, Cmds.Num() - 1, Data, Changed, ChangedIndex, 0 ); check( Changed[ChangedIndex] == 0 ); } void FRepLayout::DiffProperties_DynamicArray_r( FRepState * RepState, const int32 CmdIndex, const uint8 * RESTRICT StoredData, const uint8 * RESTRICT Data, const bool bSync, bool & bOutDifferent ) const { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; FScriptArray * Array = (FScriptArray *)Data; FScriptArray * StoredArray = (FScriptArray *)StoredData; if ( Array->Num() != StoredArray->Num() ) { bOutDifferent = true; if ( !bSync ) { UE_LOG( LogNet, Warning, TEXT( "DiffProperties_DynamicArray_r: Array sizes different: %s %i / %i" ), *Cmd.Property->GetFullName(), Array->Num(), StoredArray->Num() ); return; } if ( !( Parents[Cmd.ParentIndex].Flags & PARENT_IsLifetime ) ) { // Currently, only lifetime properties init from their defaults return; } // Make the shadow state match the actual state FScriptArrayHelper StoredArrayHelper( (UArrayProperty *)Cmd.Property, StoredData ); StoredArrayHelper.Resize( Array->Num() ); } Data = (uint8*)Array->GetData(); StoredData = (uint8*)StoredArray->GetData(); for ( int32 i = 0; i < Array->Num(); i++ ) { const int32 ElementOffset = i * Cmd.ElementSize; DiffProperties_r( RepState, CmdIndex + 1, Cmd.EndCmd - 1, StoredData + ElementOffset, Data + ElementOffset, bSync, bOutDifferent ); } } void FRepLayout::DiffProperties_r( FRepState * RepState, const int32 CmdStart, const int32 CmdEnd, const uint8 * RESTRICT StoredData, const uint8 * RESTRICT Data, const bool bSync, bool & bOutDifferent ) const { for ( int32 CmdIndex = CmdStart; CmdIndex < CmdEnd; CmdIndex++ ) { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; check( Cmd.Type != REPCMD_Return ); if ( Cmd.Type == REPCMD_DynamicArray ) { DiffProperties_DynamicArray_r( RepState, CmdIndex, StoredData + Cmd.Offset, Data + Cmd.Offset, bSync, bOutDifferent ); CmdIndex = Cmd.EndCmd - 1; // Jump past children of this array (-1 for the ++ in the for loop) continue; } const FRepParentCmd & Parent = Parents[Cmd.ParentIndex]; const FRepLayoutCmd & SwappedCmd = Cmd;//Parent.RoleSwapIndex != -1 ? Cmds[Parents[Parent.RoleSwapIndex].CmdStart] : Cmd; // Make the shadow state match the actual state at the time of send if ( !PropertiesAreIdentical( Cmd, (const void*)( Data + SwappedCmd.Offset ), (const void*)( StoredData + Cmd.Offset ) ) ) { bOutDifferent = true; if ( !bSync ) { UE_LOG( LogNet, Warning, TEXT( "DiffProperties_r: Property different: %s" ), *Cmd.Property->GetFullName() ); continue; } if ( !( Parents[Cmd.ParentIndex].Flags & PARENT_IsLifetime ) ) { // Currently, only lifetime properties init from their defaults continue; } StoreProperty( Cmd, (void*)( Data + SwappedCmd.Offset ), (const void*)( StoredData + Cmd.Offset ) ); if ( Parent.Property->HasAnyPropertyFlags( CPF_RepNotify ) ) { RepState->RepNotifies.AddUnique( Parent.Property ); } } } } bool FRepLayout::DiffProperties( FRepState * RepState, const void * RESTRICT Data, const bool bSync ) const { bool bDifferent = false; DiffProperties_r( RepState, 0, Cmds.Num() - 1, RepState->StaticBuffer.GetTypedData(), (uint8*)Data, bSync, bDifferent ); return bDifferent; } void FRepLayout::AddPropertyCmd( UProperty * Property, int32 Offset, int32 RelativeHandle, int32 ParentIndex ) { const int32 Index = Cmds.AddZeroed(); FRepLayoutCmd & Cmd = Cmds[Index]; Cmd.Property = Property; Cmd.Type = REPCMD_Property; // Initially set to generic type Cmd.Offset = Offset; Cmd.ElementSize = Property->ElementSize; Cmd.RelativeHandle = RelativeHandle; Cmd.ParentIndex = ParentIndex; // Try to special case to custom types we know about if ( Property->IsA( UStructProperty::StaticClass() ) ) { UStructProperty * StructProp = Cast< UStructProperty >( Property ); UScriptStruct * Struct = StructProp->Struct; if ( Struct->GetFName() == NAME_Vector ) { Cmd.Type = REPCMD_PropertyVector; } else if ( Struct->GetFName() == NAME_Rotator ) { Cmd.Type = REPCMD_PropertyRotator; } else if ( Struct->GetFName() == NAME_Plane ) { Cmd.Type = REPCMD_PropertyPlane; } else if ( Struct->GetName() == TEXT( "Vector_NetQuantize100" ) ) { Cmd.Type = REPCMD_PropertyVector100; } else if ( Struct->GetName() == TEXT( "Vector_NetQuantize10" ) ) { Cmd.Type = REPCMD_PropertyVector10; } else if ( Struct->GetName() == TEXT( "Vector_NetQuantizeNormal" ) ) { Cmd.Type = REPCMD_PropertyVectorNormal; } else if ( Struct->GetName() == TEXT( "Vector_NetQuantize" ) ) { Cmd.Type = REPCMD_PropertyVectorQ; } else if ( Struct->GetName() == TEXT( "UniqueNetIdRepl" ) ) { Cmd.Type = REPCMD_PropertyNetId; } else if ( Struct->GetName() == TEXT( "RepMovement" ) ) { Cmd.Type = REPCMD_RepMovement; } else { UE_LOG( LogNet, Verbose, TEXT( "AddPropertyCmd: Falling back to default type for property [%s]" ), *Cmd.Property->GetFullName() ); } } else if ( Property->IsA( UBoolProperty::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyBool; } else if ( Property->IsA( UFloatProperty::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyFloat; } else if ( Property->IsA( UIntProperty::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyInt; } else if ( Property->IsA( UByteProperty::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyByte; } else if ( Property->IsA( UObjectPropertyBase::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyObject; } else if ( Property->IsA( UNameProperty::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyName; } else if ( Property->IsA( UUInt32Property::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyUInt32; } else if ( Property->IsA( UUInt64Property::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyUInt64; } else if ( Property->IsA( UStrProperty::StaticClass() ) ) { Cmd.Type = REPCMD_PropertyString; } else { UE_LOG( LogNet, Verbose, TEXT( "AddPropertyCmd: Falling back to default type for property [%s]" ), *Cmd.Property->GetFullName() ); } } void FRepLayout::AddArrayCmd( UArrayProperty * Property, int32 Offset, int32 RelativeHandle, int32 ParentIndex ) { const int32 Index = Cmds.AddZeroed(); FRepLayoutCmd & Cmd = Cmds[Index]; Cmd.Type = REPCMD_DynamicArray; Cmd.Property = Property; Cmd.Offset = Offset; Cmd.ElementSize = Property->Inner->ElementSize; Cmd.RelativeHandle = RelativeHandle; Cmd.ParentIndex = ParentIndex; } void FRepLayout::AddReturnCmd() { const int32 Index = Cmds.AddZeroed(); FRepLayoutCmd & Cmd = Cmds[Index]; Cmd.Type = REPCMD_Return; } int32 FRepLayout::InitFromProperty_r( UProperty * Property, int32 Offset, int32 RelativeHandle, int32 ParentIndex ) { UArrayProperty * ArrayProp = Cast< UArrayProperty >( Property ); if ( ArrayProp != NULL ) { const int32 CmdStart = Cmds.Num(); RelativeHandle++; AddArrayCmd( ArrayProp, Offset + ArrayProp->GetOffset_ForGC(), RelativeHandle, ParentIndex ); InitFromProperty_r( ArrayProp->Inner, 0, 0, ParentIndex ); AddReturnCmd(); Cmds[CmdStart].EndCmd = Cmds.Num(); // Patch in the offset to jump over our array inner elements return RelativeHandle; } UStructProperty * StructProp = Cast< UStructProperty >( Property ); if ( StructProp != NULL ) { UScriptStruct * Struct = StructProp->Struct; if ( Struct->StructFlags & STRUCT_NetDeltaSerializeNative ) { // Custom delta serializers handles outside of FRepLayout return RelativeHandle; } if ( Struct->StructFlags & STRUCT_NetSerializeNative ) { RelativeHandle++; AddPropertyCmd( Property, Offset + Property->GetOffset_ForGC(), RelativeHandle, ParentIndex ); return RelativeHandle; } TArray< UProperty * > NetProperties; // Track properties so me can ensure they are sorted by offsets at the end for ( TFieldIterator It( Struct ); It; ++It ) { if ( ( It->PropertyFlags & CPF_RepSkip ) ) { continue; } NetProperties.Add( *It ); } // Sort NetProperties by memory offset struct FCompareUFieldOffsets { FORCEINLINE bool operator()( UProperty & A, UProperty & B ) const { return A.GetOffset_ForGC() < B.GetOffset_ForGC(); } }; Sort( NetProperties.GetTypedData(), NetProperties.Num(), FCompareUFieldOffsets() ); for ( int32 i = 0; i < NetProperties.Num(); i++ ) { for ( int32 j = 0; j < NetProperties[i]->ArrayDim; j++ ) { RelativeHandle = InitFromProperty_r( NetProperties[i], Offset + StructProp->GetOffset_ForGC() + j * NetProperties[i]->ElementSize, RelativeHandle, ParentIndex ); } } return RelativeHandle; } // Add actual property RelativeHandle++; AddPropertyCmd( Property, Offset + Property->GetOffset_ForGC(), RelativeHandle, ParentIndex ); return RelativeHandle; } uint16 FRepLayout::AddParentProperty( UProperty * Property, int32 ArrayIndex ) { return Parents.Add( FRepParentCmd( Property, ArrayIndex ) ); } static bool IsCustomDeltaProperty( UProperty * Property ) { UStructProperty * StructProperty = Cast< UStructProperty >( Property ); if ( StructProperty != NULL && StructProperty->Struct->StructFlags & STRUCT_NetDeltaSerializeNative ) { return true; } return false; } void FRepLayout::InitFromObjectClass( UClass * InObjectClass ) { RoleIndex = -1; RemoteRoleIndex = -1; FirstNonCustomParent = -1; int32 RelativeHandle = 0; int32 LastOffset = -1; Parents.Empty(); for ( int32 i = 0; i < InObjectClass->ClassReps.Num(); i++ ) { UProperty * Property = InObjectClass->ClassReps[i].Property; const int32 ArrayIdx = InObjectClass->ClassReps[i].Index; check( Property->PropertyFlags & CPF_Net ); const int32 ParentHandle = AddParentProperty( Property, ArrayIdx ); check( ParentHandle == i ); check( Parents[i].Property->RepIndex + Parents[i].ArrayIndex == i ); Parents[ParentHandle].CmdStart = Cmds.Num(); RelativeHandle = InitFromProperty_r( Property, Property->ElementSize * ArrayIdx, RelativeHandle, ParentHandle ); Parents[ParentHandle].CmdEnd = Cmds.Num(); Parents[ParentHandle].Flags |= PARENT_IsConditional; if ( Parents[i].CmdEnd > Parents[i].CmdStart ) { check( Cmds[Parents[i].CmdStart].Offset >= LastOffset ); // >= since bool's can be combined LastOffset = Cmds[Parents[i].CmdStart].Offset; } // Setup flags if ( IsCustomDeltaProperty( Property ) ) { Parents[ParentHandle].Flags |= PARENT_IsCustomDelta; } if ( Property->GetPropertyFlags() & CPF_Config ) { Parents[ParentHandle].Flags |= PARENT_IsConfig; } // Hijack the first non custom property for identifying this as a rep layout block if ( FirstNonCustomParent == -1 && Property->ArrayDim == 1 && ( Parents[ParentHandle].Flags & PARENT_IsCustomDelta ) == 0 ) { FirstNonCustomParent = ParentHandle; } // Find Role/RemoteRole property indexes so we can swap them on the client if ( Property->GetFName() == NAME_Role ) { check( RoleIndex == -1 ); check( Parents[ParentHandle].CmdEnd == Parents[ParentHandle].CmdStart + 1 ); RoleIndex = ParentHandle; } if ( Property->GetFName() == NAME_RemoteRole ) { check( RemoteRoleIndex == -1 ); check( Parents[ParentHandle].CmdEnd == Parents[ParentHandle].CmdStart + 1 ); RemoteRoleIndex = ParentHandle; } } // Make sure it either found both, or didn't find either check( ( RoleIndex == -1 ) == ( RemoteRoleIndex == -1 ) ); // This is so the receiving side can swap these as it receives them if ( RoleIndex != -1 ) { Parents[RoleIndex].RoleSwapIndex = RemoteRoleIndex; Parents[RemoteRoleIndex].RoleSwapIndex = RoleIndex; } AddReturnCmd(); // Initialize lifetime props TArray< FLifetimeProperty > LifetimeProps; // Properties that replicate for the lifetime of the channel UObject * Object = InObjectClass->GetDefaultObject(); Object->GetLifetimeReplicatedProps( LifetimeProps ); if ( LifetimeProps.Num() > 0 ) { UnconditionalLifetime.Empty(); ConditionalLifetime.Empty(); // Fix Lifetime props to have the proper index to the parent for ( int32 i = 0; i < LifetimeProps.Num(); i++ ) { // Store the condition on the parent in case we need it Parents[LifetimeProps[i].RepIndex].Condition = LifetimeProps[i].Condition; if ( Parents[LifetimeProps[i].RepIndex].Flags & PARENT_IsCustomDelta ) { continue; // We don't handle custom properties in the FRepLayout class } Parents[LifetimeProps[i].RepIndex].Flags |= PARENT_IsLifetime; if ( LifetimeProps[i].RepIndex == RemoteRoleIndex ) { // We handle remote role specially, since it can change between connections when downgraded // So we force it on the conditional list check( LifetimeProps[i].Condition == COND_None ); LifetimeProps[i].Condition = COND_Custom; ConditionalLifetime.AddUnique( LifetimeProps[i] ); continue; } if ( LifetimeProps[i].Condition == COND_None ) { // These properties are simple, and can benefit from property compare sharing UnconditionalLifetime.AddUnique( LifetimeProps[i].RepIndex ); Parents[LifetimeProps[i].RepIndex].Flags &= ~PARENT_IsConditional; continue; } // The rest are conditional // These properties are eventually conditionally copied to the RepState ConditionalLifetime.AddUnique( LifetimeProps[i] ); } } Owner = InObjectClass; } void FRepLayout::InitFromFunction( UFunction * InFunction ) { int32 RelativeHandle = 0; for ( TFieldIterator It( InFunction ); It && ( It->PropertyFlags & ( CPF_Parm | CPF_ReturnParm ) ) == CPF_Parm; ++It ) { for ( int32 ArrayIdx = 0; ArrayIdx < It->ArrayDim; ++ArrayIdx ) { const int32 ParentHandle = AddParentProperty( *It, ArrayIdx ); Parents[ParentHandle].CmdStart = Cmds.Num(); RelativeHandle = InitFromProperty_r( *It, It->ElementSize * ArrayIdx, RelativeHandle, ParentHandle ); Parents[ParentHandle].CmdEnd = Cmds.Num(); } } Owner = InFunction; } void FRepLayout::InitFromStruct( UStruct * InStruct ) { int32 RelativeHandle = 0; for ( TFieldIterator It( InStruct ); It; ++It ) { if ( It->PropertyFlags & CPF_RepSkip ) { continue; } for ( int32 ArrayIdx = 0; ArrayIdx < It->ArrayDim; ++ArrayIdx ) { const int32 ParentHandle = AddParentProperty( *It, ArrayIdx ); Parents[ParentHandle].CmdStart = Cmds.Num(); RelativeHandle = InitFromProperty_r( *It, It->ElementSize * ArrayIdx, RelativeHandle, ParentHandle ); Parents[ParentHandle].CmdEnd = Cmds.Num(); } } Owner = InStruct; } void FRepLayout::SerializeProperties_DynamicArray_r( FArchive & Ar, UPackageMap * Map, const int32 CmdIndex, uint8 * Data, bool & bHasUnmapped ) const { const FRepLayoutCmd & Cmd = Cmds[ CmdIndex ]; FScriptArray * Array = (FScriptArray *)Data; // Read array num uint16 ArrayNum = Array->Num(); Ar << ArrayNum; const int MAX_ARRAY_SIZE = 2048; if ( ArrayNum > MAX_ARRAY_SIZE ) { UE_LOG( LogNetTraffic, Error, TEXT( "SerializeProperties_DynamicArray_r: ArrayNum > MAX_ARRAY_SIZE (%s)" ), *Cmd.Property->GetName() ); Ar.SetError(); return; } const int MAX_ARRAY_MEMORY = 1024 * 64; if ( (int32)ArrayNum * Cmd.ElementSize > MAX_ARRAY_MEMORY ) { UE_LOG( LogNetTraffic, Error, TEXT( "SerializeProperties_DynamicArray_r: ArrayNum * Cmd.ElementSize > MAX_ARRAY_MEMORY (%s)" ), *Cmd.Property->GetName() ); Ar.SetError(); return; } if ( Ar.IsLoading() && ArrayNum != Array->Num() ) { // If we are reading, size the array to the incoming value FScriptArrayHelper ArrayHelper( (UArrayProperty *)Cmd.Property, Data ); ArrayHelper.Resize( ArrayNum ); } Data = (uint8*)Array->GetData(); for ( int32 i = 0; i < Array->Num() && !Ar.IsError(); i++ ) { SerializeProperties_r( Ar, Map, CmdIndex + 1, Cmd.EndCmd - 1, Data + i * Cmd.ElementSize, bHasUnmapped ); } } void FRepLayout::SerializeProperties_r( FArchive & Ar, UPackageMap * Map, const int32 CmdStart, const int32 CmdEnd, void * Data, bool & bHasUnmapped ) const { for ( int32 CmdIndex = CmdStart; CmdIndex < CmdEnd && !Ar.IsError(); CmdIndex++ ) { const FRepLayoutCmd & Cmd = Cmds[CmdIndex]; check( Cmd.Type != REPCMD_Return ); if ( Cmd.Type == REPCMD_DynamicArray ) { SerializeProperties_DynamicArray_r( Ar, Map, CmdIndex, (uint8*)Data + Cmd.Offset, bHasUnmapped ); CmdIndex = Cmd.EndCmd - 1; // The -1 to handle the ++ in the for loop continue; } #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) if (CVarDoReplicationContextString->GetInt() > 0) { Map->SetDebugContextString( FString::Printf(TEXT("%s - %s"), *Owner->GetPathName(), *Cmd.Property->GetPathName() ) ); } #endif if ( !Cmd.Property->NetSerializeItem( Ar, Map, (void*)( (uint8*)Data + Cmd.Offset ) ) ) { bHasUnmapped = true; } #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) if (CVarDoReplicationContextString->GetInt() > 0) { Map->ClearDebugContextString(); } #endif } } void FRepLayout::SendPropertiesForRPC( UObject * Object, UFunction * Function, UActorChannel * Channel, FNetBitWriter & Writer, void * Data ) const { check( Function == Owner ); for ( int32 i = 0; i < Parents.Num(); i++ ) { /* if ( !Writer.PackageMap->SupportsObject( Parents[i].Property ) ) { continue; } */ bool Send = true; if ( !Cast( Parents[i].Property ) ) { // check for a complete match, including arrays // (we're comparing against zero data here, since // that's the default.) Send = false; if ( !Parents[i].Property->Identical_InContainer( Data, NULL, Parents[i].ArrayIndex ) ) { Send = true; } Writer.WriteBit( Send ? 1 : 0 ); } if ( Send ) { bool bHasUnmapped = false; SerializeProperties_r( Writer, Writer.PackageMap, Parents[i].CmdStart, Parents[i].CmdEnd, Data, bHasUnmapped ); if ( bHasUnmapped ) { // RPC function is sending an unmapped object... UE_LOG( LogNetTraffic, Log, TEXT( "Actor[%d] %s RPC %s parameter %s was sent while unmapped! This call may not be correctly handled on the receiving end." ), Channel->ChIndex, *Object->GetName(), *Function->GetName(), *Parents[i].Property->GetName() ); } } } } void FRepLayout::ReceivePropertiesForRPC( UObject * Object, UFunction * Function, UActorChannel * Channel, FNetBitReader & Reader, void * Data ) const { check( Function == Owner ); for ( int32 i = 0; i < Parents.Num(); i++ ) { /* if ( !Reader.PackageMap->SupportsObject( Parents[i].Property ) ) { continue; } */ if ( Cast( Parents[i].Property ) || Reader.ReadBit() ) { bool bHasUnmapped = false; SerializeProperties_r( Reader, Reader.PackageMap, Parents[i].CmdStart, Parents[i].CmdEnd, Data, bHasUnmapped ); if ( Reader.IsError() ) { return; } if ( bHasUnmapped ) { UE_LOG( LogNetTraffic, Log, TEXT( "Unable to resolve RPC parameter. Object[%d] %s. Function %s. Parameter %s." ), Channel->ChIndex, *Object->GetName(), *Function->GetName(), *Parents[i].Property->GetName() ); } } } } void FRepLayout::SerializePropertiesForStruct( UStruct * Struct, FArchive & Ar, UPackageMap * Map, void * Data, bool & bHasUnmapped ) const { check( Struct == Owner ); for ( int32 i = 0; i < Parents.Num(); i++ ) { /* if ( Map != NULL && !Map->SupportsObject( Parents[i].Property ) ) { continue; } */ SerializeProperties_r( Ar, Map, Parents[i].CmdStart, Parents[i].CmdEnd, Data, bHasUnmapped ); if ( Ar.IsError() ) { return; } } } void FRepLayout::RebuildConditionalProperties( FRepState * RESTRICT RepState, const FRepChangedPropertyTracker & ChangedTracker, const FReplicationFlags & RepFlags ) const { SCOPE_CYCLE_COUNTER( STAT_NetRebuildConditionalTime ); //UE_LOG( LogNet, Warning, TEXT( "Rebuilding custom properties [%s]" ), *Owner->GetName() ); // Setup condition map bool ConditionMap[COND_Max]; const bool bIsInitial = RepFlags.bNetInitial ? true : false; const bool bIsOwner = RepFlags.bNetOwner ? true : false; const bool bIsSimulated = RepFlags.bNetSimulated ? true : false; const bool bIsPhysics = RepFlags.bRepPhysics ? true : false; ConditionMap[COND_None] = true; ConditionMap[COND_InitialOnly] = bIsInitial; ConditionMap[COND_OwnerOnly] = bIsOwner; ConditionMap[COND_SkipOwner] = !bIsOwner; ConditionMap[COND_SimulatedOnly] = bIsSimulated; ConditionMap[COND_AutonomousOnly] = !bIsSimulated; ConditionMap[COND_SimulatedOrPhysics] = bIsSimulated || bIsPhysics; ConditionMap[COND_InitialOrOwner] = bIsInitial || bIsOwner; ConditionMap[COND_Custom] = true; RepState->ConditionalLifetime.Empty(); for ( int32 i = 0; i < ConditionalLifetime.Num(); i++ ) { if ( !ConditionMap[ConditionalLifetime[i].Condition] ) { continue; } if ( !ChangedTracker.Parents[ConditionalLifetime[i].RepIndex].Active ) { continue; } RepState->ConditionalLifetime.Add( ConditionalLifetime[i].RepIndex ); } RepState->RepFlags = RepFlags; } void FRepLayout::InitChangedTracker( FRepChangedPropertyTracker * ChangedTracker ) const { ChangedTracker->Parents.SetNum( Parents.Num() ); for ( int32 i = 0; i < Parents.Num(); i++ ) { ChangedTracker->Parents[i].IsConditional = ( Parents[i].Flags & PARENT_IsConditional ) ? 1 : 0; } } void FRepLayout::InitRepState( FRepState * RepState, UClass * InObjectClass, uint8 * Src, TSharedPtr< FRepChangedPropertyTracker > & InRepChangedPropertyTracker ) const { RepState->StaticBuffer.Empty(); RepState->StaticBuffer.AddZeroed( InObjectClass->GetDefaultsCount() ); // Construct the properties ConstructProperties( RepState ); // Init the properties InitProperties( RepState, Src ); RepState->RepChangedPropertyTracker = InRepChangedPropertyTracker; check( RepState->RepChangedPropertyTracker->Parents.Num() == Parents.Num() ); // Start out the conditional props based on a default RepFlags struct // It will rebuild if it ever changes RebuildConditionalProperties( RepState, *InRepChangedPropertyTracker.Get(), FReplicationFlags() ); } void FRepLayout::ConstructProperties( FRepState * RepState ) const { uint8 * StoredData = RepState->StaticBuffer.GetTypedData(); // Construct all items for ( int32 i = 0; i < Parents.Num(); i++ ) { // Only construct the 0th element of static arrays (InitializeValue will handle the elements) if ( Parents[i].ArrayIndex == 0 ) { PTRINT Offset = Parents[i].Property->ContainerPtrToValuePtr( StoredData ) - StoredData; check( Offset >= 0 && Offset < RepState->StaticBuffer.Num() ); Parents[i].Property->InitializeValue( StoredData + Offset ); } } } void FRepLayout::InitProperties( FRepState * RepState, uint8 * Src ) const { uint8 * StoredData = RepState->StaticBuffer.GetTypedData(); // Init all items for ( int32 i = 0; i < Parents.Num(); i++ ) { // Only copy the 0th element of static arrays (CopyCompleteValue will handle the elements) if ( Parents[i].ArrayIndex == 0 ) { PTRINT Offset = Parents[i].Property->ContainerPtrToValuePtr( StoredData ) - StoredData; check( Offset >= 0 && Offset < RepState->StaticBuffer.Num() ); Parents[i].Property->CopyCompleteValue( StoredData + Offset, Src + Offset ); } } } void FRepLayout::DestructProperties( FRepState * RepState ) const { uint8 * StoredData = RepState->StaticBuffer.GetTypedData(); // Destruct all items for ( int32 i = 0; i < Parents.Num(); i++ ) { // Only copy the 0th element of static arrays (DestroyValue will handle the elements) if ( Parents[i].ArrayIndex == 0 ) { PTRINT Offset = Parents[i].Property->ContainerPtrToValuePtr( StoredData ) - StoredData; check( Offset >= 0 && Offset < RepState->StaticBuffer.Num() ); Parents[i].Property->DestroyValue( StoredData + Offset ); } } } void FRepLayout::GetLifetimeCustomDeltaProperties( TArray< int32 > & OutCustom ) { OutCustom.Empty(); for ( int32 i = 0; i < Parents.Num(); i++ ) { if ( Parents[i].Flags & PARENT_IsCustomDelta ) { check( Parents[i].Property->RepIndex + Parents[i].ArrayIndex == i ); OutCustom.AddUnique( i ); } } } FRepState::~FRepState() { if (RepLayout.IsValid() && StaticBuffer.Num() > 0) { RepLayout->DestructProperties( this ); } }