Core Redirect Engine Component to Project Component

I’m trying to use a Core Redirect to replace a C++ Engine component (in this case USpotLightComponent) with an extended version in my project.

According to the Core Redirect documentation (https://dev.epicgames.com/documentation/en-us/unreal-engine/core-redirects-in-unreal-engine), this is the exact use case described in the InstanceOnly=true case for Class redirects.

If present and set to true, indicates that the original class still exists and can be referenced, but any existing instances of the old class (such as Actors or Components placed in Levels) should be remapped to the new class. This is especially useful when your project has a specialized version of a class that exists in the engine, but your Levels are full of instances of the original class, and you want to change them all to the project-specific version.

However, when I load a level that has such a component, I see the linker fixing up the export, but the import fails with this error:

Failed import: class 'MySpotLightComponent' name 'LightComponent0' outer 'SpotLight_1'. There is another object (of 'SpotLightComponent' class) at the path.What am I missing here?

Thanks,

Ernesto.

Steps to Reproduce
Create a simple extended class that specializes an Engine class. ie:

#include "CoreMinimal.h"
#include "Components/SpotLightComponent.h"
#include "LightComponents.generated.h"

UCLASS(Blueprintable, ClassGroup = (Lights), hidecategories = (Object), editinlinenew, meta = (BlueprintSpawnableComponent))
class MYGAME_API UMySpotLightComponent : public USpotLightComponent
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Flickering", meta = (DisplayName = "Enable"))
	bool bLightFlickeringEnabled = false;
};

Then add a redirector in DefaultEngine.ini:

[CoreRedirects]
+ClassRedirects=(OldName="/Script/Engine.SpotLightComponent",NewName="/Script/MyGame.MySpotLightComponent",InstanceOnly=true)

Start the Editor and load a level that has a USpotlightComponent. On the Output Log, it shows:

LogLinker: FLinkerLoad::FixupExportMap() - Pkg</Game/Levels/L_MainMenu> [Obj<LightComponent0> Cls<SpotLightComponent> ClsPkg</Script/Engine>] -> [Obj<LightComponent0> Cls<MySpotLightComponent> ClsPkg</Script/MyGame>]
LogLinker: Error: [AssetLog] D:\UE5\MyGame\Content\Levels\L_MainMenu.umap: Failed import: class 'MySpotLightComponent' name 'LightComponent0' outer 'SpotLight_1'. There is another object (of 'SpotLightComponent' class) at the path.

Hello, I’ve started investigating the issue. The good news is that when I tried the repro steps on //UE5/Main, the ClassRedirect with InstanceOnly=true appears to work correctly. I confirmed by checking the saved level’s actor’s SpotLightComponent is indeed of type MySpotLightComponent in the details panel and by printing its class at runtime. [Image Removed]

So I’ll focus my efforts on investigating which change between UE 5.5 and 5.7 fixes the feature. If you still have the UE 5.5 repro, can you share the callstack that prints this error?

LogLinker: Error: [AssetLog] D:\UE5\MyGame\Content\Levels\L_MainMenu.umap: Failed import: class 'MySpotLightComponent' name 'LightComponent0' outer 'SpotLight_1'. There is another object (of 'SpotLightComponent' class) at the path.That helps me find the relevant change.

Kyle mentioned: "The original spotlight component class is still being created in C++ on the spotlight actor, which is why you’re receiving a linker error. "

I want to add to that that I don’t think the original class being instanced is a problem: objects can exist and be renamed out of the way. In some contexts, instantiating an outdated component makes sense to deserialize data stored on disk.

I’ll continue investigating early next week to find what may have fixed the behavior. If you have the UE 5.5 callstack for that error ready that would help, otherwise I’ll repro again in 5.5 when I continue. Have a nice weekend!

Hello again. I’ve confirmed that indeed without engine modifications component class overrides are still not compatible with InstanceOnly ClassRedirects in UE 5.7. I have also experimented with your engine modification in the latest engine version (UE 5.7+). Here is what I’ve found.

The original warning no longer happens in UE 5.7

This linker error, I was able to reproduce in UE 5.5 from your repro steps but not in 5.7:

LogLinker: Error: [AssetLog] D:\UE5\MyGame\Content\Levels\L_MainMenu.umap: Failed import: class 'MySpotLightComponent' name 'LightComponent0' outer 'SpotLight_1'. There is another object (of 'SpotLightComponent' class) at the path.The reason is CL 37399091 which contributes towards retaining an object’s saved data when its class changes. That said, the SpotLight actors in your repro steps still keep a normal SpotLightComponent instead of the class override.

Thoughts on your patch

First of all, your patch works on my end to replace the SpotLightComponents with MySpotLightComponents on actors. I also observed that in UE 5.7, indeed the components usually get destroyed unless you hack with object flags. There are two problems which I foresee here, which are:

  • Actors that are saved with SpotLightComponents get their binary data deserialized as MySpotLightComponents. This can cause issues down the road if you would ever give MySpotLightComponent a custom Serialize() function where SpotLightComponent serialized data gets deserialized in a wrong order. Now, you have a versioning burden whenever you have a custom Serialize function but ideally when both classes are still present the engine code would migrate data from [the existing object loaded from disk in its original class] into [a new object of the new class] via UEngine::CopyPropertiesForUnrelatedObjects(). Since both classes are present with your use case, I think for an engine solution that would be the approach we would take. You can keep your patch though as long as aware of that versioning burden.
  • Removing RF_DefaultSubObject from the object just to avoid being destroyed prematurely is bound to have knockon effects. For example UEngine::CopyPropertiesForUnrelatedObjects checks the RF_DefaultSubObject flag to decide which subobjects to migrate data for. Things like duplicating the map could go wrong now, because the SpotLight actor in the new map might not have copied the values for MySpotLightComponent due to missing the flag. I haven’t tested this out, but it’s this type of bug that I would expect from clearing the RF_DefaultSubObject flag.

“At least this is now documented here in case somebody else runs into the same problem.”

Indeed, thank you for bringing this up.

Hi again, one more update after collecting opinions from team mates on whether and how we’d address this: it’s unlikely that we would fix this use case (applying InstanceOnly ClassRedirects in ObjectInitializer) or fix up other places in native engine code where the specific old class is being spawned. Just letting you know for your own planning purposes.

Hello Ernesto,

After doing some tests of my own, I was able to surpass this error by changing the syntax of the ClassRedirect in the DefaultEngine.ini to:

+ClassRedirects=(OldName="SpotLightComponent",NewName="MySpotLightComponent",InstanceOnly=true)
// or this
+ClassRedirects=(OldName="/Script/Engine.SpotLightComponent",NewName="/Script/Engine.MySpotLightComponent",InstanceOnly=true)

It should be noted too that when I attempted to reproduce your issue I was able to see the error, but “MyProjectName.MySpotLightComponent” worked for me in place of NewName.

You could attempt to create a new spotlight with your version of the component attached to it, and see if the behavior changes?

Let me know if that solves the issue for you, or if a different syntax is required.

Thanks,

Kyle B.

Hi Kyle.

Thanks for your support. Unfortunately it doesn’t. While the error itself is gone, it just silently fails to create the component.

I tried:

[OldName="SpotLightComponent",NewName="MySpotLightComponent",InstanceOnly=true)
(OldName="Script/Engine.SpotLightComponent",NewName="Script/Engine.MySpotLightComponent",InstanceOnly=true)
(OldName="Script/Engine.SpotLightComponent",NewName="MyGame.MySpotLightComponent",InstanceOnly=true)

If you look at the linker message with any of those:

LogLinker: FLinkerLoad::FixupExportMap() - Pkg</Game/Levels/L_MainMenu> [Obj<LightComponent0> Cls<SpotLightComponent> ClsPkg</Script/Engine>] -> [Obj<LightComponent0> Cls<MySpotLightComponent> ClsPkg</Script/Engine>]Then you can see it’s looking for the class in the Engine, which is not correct, and it’s probably why it fails. Now I wonder if this broke when the Engine was changed from using classes to asset class paths during UE 5.1.

I can always go and edit the components in the Engine directly, but the whole point of this exercise is to try to avoid that.

Thanks,

Ernesto.

Hello Ernesto,

Thank you for the response, the screenshot helps to illuminate the issue!

The original spotlight component class is still being created in C++ on the spotlight actor, which is why you’re receiving a linker error. You can redirect the spotlight component class if it is added to a non-spotlight actor, or you can redirect the entire spotlight actor class instead. This will replace the spotlight actor in the map files with your version of the spotlight actor, and then you can successfully redirect the spotlight component class.

To do this with blueprints, you can create a blueprint subclass of the spotlight actor, and swap out the spotlight component class with your own. Then, if you redirect to this blueprint class, e.g.:

+ClassRedirects=(OldName="/Script/Engine.SpotLight",NewName="/Game/<YOUR_FILE_PATH>/MySpotLight.MySpotLight_C",InstanceOnly=true)All existing spotlight actors will then have the correct spotlight component class.

In C++, the base engine spotlight class is not exported. To export it would require custom engine modifications: you’d need to remove the MinimalAPI within the UCLASS and add ENGINE_API after the class keyword. Then, you can subclass it and use FObjectInitializer to swap out the subcomponent, and finally redirect to your spotlight class.

Afterwards, the original core redirect path you were using should work correctly for the spotlight component.

Let me know if this works for you, and if you have any other questions!

Thanks,

Kyle B.

Hi Kyle.

Let me first state that I appreciate your support, and your post led me in the right direction.

You’re correct that the original component class is still being created in C++, and that’s actually the problem. The reason this happens is due to the FObjectInitializer overrides not honoring class redirects for instanced objects.

I’ve patched up FObjectInitializer::CreateDefaultSubobject() to fix up the class after the SubobjectOverrides.Get() call, and that actually causes the component to build properly with my redirected class and my initial redirect. ie:

FOverrides::FOverrideDetails ComponentOverride = SubobjectOverrides.Get(SubobjectFName, ReturnType, ClassToCreateByDefault, !bIsRequired);
 
//// PATCH START
if (ComponentOverride.Class)
{
	// Look for class redirectors
	FString NewPath = FLinkerLoad::FindNewPathNameForClass(ComponentOverride.Class->GetClassPathName().ToString(), !Outer->IsTemplate());
	if (!NewPath.IsEmpty())
	{
		ComponentOverride.Class = FindObject<UClass>(nullptr, *NewPath);
	}
}
//// PATCH END

The last bit of the puzzle with this is on the Editor, when AActor::RerunConstructionScripts() is called. The DestroyConstructedComponents() function has this bit of code:

if (!bDestroyComponent)
{
	// check for orphaned natively created components:
	if (Component->CreationMethod == EComponentCreationMethod::Native && Component->HasAnyFlags(RF_DefaultSubObject))
	{
		UObject* ComponentArchetype = Component->GetArchetype();
		if (ComponentArchetype == ComponentArchetype->GetClass()->ClassDefaultObject)
		{
			bDestroyComponent = true;
		}
	}
}

This ends up destroying my overriden component. I worked around this for now by removing the RF_DefaultSubObject flag in my component’s constructor, and that makes it all work, but curious if anybody knows of a better way to do this, and some sort of explanation about the Archetype and Archetype’s CDO voodoo going on there.

Thanks,

Ernesto.

Hello Ernesto,

I’m glad we were able to find a good workaround for this!

I’ll go ahead and escalate this case to Epic, as I think they would have a better understanding of the inner workings of the archetype (and possibly a better workaround).

Thanks,

Kyle B.

Hi [Zhi Kang [Content removed]

I think the way you set this up with a BP and a Spotlight Component works, but if you use a ASpotLight actor instead, that defines the component class via an override, it’s when it breaks. See the ASpotLight constructor:

ASpotLight::ASpotLight(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.SetDefaultSubobjectClass<USpotLightComponent>(TEXT("LightComponent0")))

Might just be a corner case that isn’t handling the class redirect. Let me know if you can’t repro with that case, and I’ll post the callstack you requested.

Ernesto.

Thanks for your support, [Zhi Kang [Content removed]

I’ll just tweak the engine for our case. It’s a bummer, but not that big of a deal. At least this is now documented here in case somebody else runs into the same problem.

Thanks again,

Ernesto.