Hot reload on native actor component can lead to Blueprint corruption and data loss

Sep 16, 2020.Knowledge
UE-86465 - Hot reload on Native Actor Component can lead to Blueprint corruption and data loss
Summary: Hot reload of a UActorComponent/USceneComponent-based native class will currently corrupt any loaded Blueprint that incorporates a component of that type in its hierarchy or AddComponent node if the Blueprint is saved after hot reload succeeds. For the bug to occur, the loaded Blueprint must also be either:

a) Open in the Blueprint editor (i.e. has an active preview instance), OR

b) Instanced in the current level somewhere.

The end result is that after hot reload, the archetype gets replaced by a reference to the hot reloaded class’s CDO instead. If the user saves the Blueprint after that, modified component data can no longer persist. The only recourse is to destroy/recreate or duplicate the component or (possibly) duplicate the BP to a new asset; a reload will not fix it. This affects at least engine version 4.21 and later. This issue doesn’t have a clear fix at the moment and it’s currently backlogged with a number of other hot reload bugs.

Details: The root cause is archetype lookup failure in UBlueprintGeneratedClass::FindArchetype() from inside of CPFUO during ReplaceObjectHelper() in KismetReinstanceUtilities.cpp during the hot reload of the native component class:

InstancedPropertyUtils::FInstancedPropertyMap InstancedPropertyMap;
InstancedPropertyUtils::FArchiveInstancedSubObjCollector InstancedSubObjCollector(OldObject, InstancedPropertyMap);
==> UEditorEngine::CopyPropertiesForUnrelatedObjects(OldObject, NewUObject);
InstancedPropertyUtils::FArchiveInsertInstancedSubObjects InstancedSubObjSpawner(NewUObject, InstancedPropertyMap);

When that gets called to update the new preview Actor’s instance of the hotreloaded component class, CPFUO attempts to look up the archetype for the new instance:

// Gather references to old instances or objects that need to be replaced after we serialize in saved data
TMap<UObject*, UObject*> ReferenceReplacementMap;
ReferenceReplacementMap.Add(OldObject, NewObject);
==> ReferenceReplacementMap.Add(OldObject->GetArchetype(), NewObject->GetArchetype());

This fails to pass the type check inside of UBlueprintGeneratedClass::FindArchetype():

if (SCSNode->ComponentTemplate && SCSNode->ComponentTemplate->IsA(ArchetypeClass))
Archetype = SCSNode->ComponentTemplate;

At this point, ArchetypeClass is set to the hotreloaded (new) class, but the reference stored in the SCS node’s component template property has not yet been replaced, so it still references the old archetype instance of the old class. Since it fails to match the node, GetArchetype() falls back to its default case and returns the native CDO for the hotreloaded (new) class. Thus, in the ReferenceReplacementMap that’s populated by CPFUO and broadcast back out to the reinstancer via delegate, we (incorrectly) end up with:

SCS node template (old) → new component class CDO

When it should be:

SCS node template (old) → SCS node template (new)

The reinstancer keeps track of this mapping here:

struct FObjectRemappingHelper
void OnObjectsReplaced(const TMap<UObject*, UObject*>& InReplacedObjects)
==> ReplacedObjects.Append(InReplacedObjects);

TMap<UObject*, UObject*> ReplacedObjects;

} ObjectRemappingHelper;

And it later stomps over the correct mapping here:


After that comes reference replacement, and the SCS node winds up referencing the hotreloaded class’s CDO as a result. That means that the user ends up modifying the native class CDO instead of the reinstanced template object when editing. That’s why the reset arrow stops appearing when editing defaults, because the delta is comparing against the same object (CDO). And that’s why the defaults aren’t saved, because the native CDO is not serialized, so it results in permanent data loss. It can also cause other odd behaviors in the editor session since a native CDO is potentially now being modified inside the Blueprint editor unintentionally.