Supporting UE 5.3 to 5.8 in plugin code
Writing MifBridge C++ that compiles on every supported engine, Unreal Engine 5.3 through 5.8: the six ways an engine API differs between versions, and a compile test that settles it. This is for a developer or an agent editing the plugin's source from a GitHub checkout, and a person can point their agent at this page.
MifBridge supports UE 5.3 through 5.8, cooked and uncooked. Every handler has to compile on all six. Most examples below are 5.3 against 5.7, where they were found, but a change can land in any version between. A guard written as "5.7 or later" has to be checked on 5.4, 5.5 and 5.6 as well, because one written that way once missed all three.
The failure is always the same shape: a symbol that behaves differently between engine trees. It arrives from six directions, and only the first two can be found by reading headers.
A: 5.3 has it, 5.7 deleted it
You write against the editor in front of you, it compiles, and the 5.7 build breaks.
| Symbol | What happened |
|---|---|
IAssetRegistry::GetAssetsByClass(FName, ...) |
Deprecated in 5.3, deleted in 5.7. Pass GetClassPathName(). |
UObject::IsPendingKillOrUnreachable() |
Gone in 5.7. Use IsValid(Obj). |
UDataLayerSubsystem::GetDataLayerInstances |
Deprecated in 5.3. Use UDataLayerManager. |
A 5.3 deprecation warning is a 5.7 build break. Treat every one as an error.
B: 5.7 has it, 5.3 never did
Nothing warns you. If you verify an API by reading the 5.7 tree, you can pick a member 5.3 never had. Nothing was deprecated, so there is no notice. It compiles on 5.7 and fails outright on 5.3.
UGameFeaturesSubsystem::GetPluginState is the example: it returns the exact state enum in one call, and it would not build on 5.3. The endpoint derives the state from four predicates that exist in both engines, and says so in its own response, so no caller mistakes a derived answer for the engine's.
The four that reading cannot find
C: both have it, and 5.7 rejects it anyway
The code is identical and legal, and the newer compiler is stricter.
// C2445 on 5.7, fine on 5.3
UClass* C = Blueprint->ParentClass ? Blueprint->ParentClass : UObject::StaticClass();
ParentClass is a TSubclassOf<UObject> and the other branch is a raw UClass*. 5.7 will not pick a common type. .Get() fixes it and does nothing on 5.3. There is nothing to search for. The only signal is a compiler that has not run.
D: same name, different kind of thing
// HttpRequestHandler.h:19
5.3: typedef TFunction<bool(...)> FHttpRequestHandler;
5.7: using FHttpRequestHandler = TDelegate<bool(...)>;
TFunction converts from a bare lambda. TDelegate needs CreateLambda. The trap is where it does not appear: IHttpRouter.h, the header the error points at, is byte-identical between the trees.
If you shim this with a macro, make the macro variadic. A lambda's parameter list contains commas, so a single-parameter macro sees two arguments and expands to garbage that corrupts everything after it. One such mistake produced seven errors, including an "illegal else without matching if" ninety lines above anything that had changed.
E: same type, same name, different header
FStringOutputDevice // 5.3: Containers/UnrealString.h, free via CoreMinimal
// 5.7: Misc/StringOutputDevice.h, no longer transitive
The include is required on 5.7 and a fatal C1083 on 5.3. Nothing was deprecated or deleted. A type moved. FInstancedStruct did the same: it is an experimental plugin in 5.3 (Plugins/Experimental/StructUtils) and core in 5.7.
F: same name, same arity, same return type, different parameter type
This is the sharpest, because every check short of a compiler passes it.
5.3: int32 AddSolver(TSubclassOf<UIKRigSolver> InSolverClass) const;
5.7: int32 AddSolver(const FString InIKRigSolverType) const;
A search finds it in both trees, and a presence check passes, but it cannot compile. Underneath is an architecture change: UE 5.6 moved IK Rig solvers from UObject to UStruct, and UIKRigSolver survives only as a legacy loading shim. Treat a renamed header in a newer engine as a question: what moved, and why did it need a new name?
The rule this reduces to
Reading finds symbols that were deleted. It reliably misses symbols that changed shape. So:
- Verify every engine symbol in both trees, and record the line numbers in the file. Not one tree, and not just the tree of the editor in front of you.
- A search tells you a member exists. It does not tell you which class owns it or whether it is public. Two
UWaterBodyComponentmembers were once attributed toAWaterBodythis way. Find the declaration and its access specifier. - Then run a compiler. Build a throwaway one-module project on each engine you claim. Careful reading is not a build.
Three traps in the tooling
Build.batreturns exit code 0 on a build that printsResult: Failed. Search the log forResult: Failedand forerror, and check that the binary's modified time moved.- UE compiles C4668, an undefined macro in an
#if, as an error. AMIF_WITH_*guard is only safe becauseBuild.csdefines it on both branches. Adding a guard to a.cppwithout adding the definition fails the build. It does not take the false branch. - Junction the plugin's
Source/folder only, never its root. UBT writes plugin output to<Project>/Plugins/<Name>/Binaries, so a root junction sends one engine's binaries into the other's folder. That replaced a working 5.3 DLL with a 5.7 one while reporting success throughout.
When a guard is unavoidable
MifBridgeVersion.h provides MIF_ENGINE_AT_LEAST(Major, Minor), MIF_ENGINE_BEFORE and MIF_ENGINE_5_7_PLUS. Use them sparingly, because a guarded branch is code that only one build ever compiles. In order of preference:
- Use an API present in both engines. Verify it in both trees and record the line numbers.
- Keep the common subset as the baseline and let the guard add to it. Never let the two branches produce differently shaped output. If 5.3 returns one set of fields and 5.7 another, every caller has to branch on engine version and the bridge has exported its problem.
- Guard a whole feature out only when it is impossible on the older engine, and make that build refuse by name instead of silently omitting the endpoint.
Where the two engines disagree about a concept, report which model answered rather than hiding it. list_ik_solver_types returns a solverModel field, and list_retarget_chain_mapping a runtimeProbeModel, so a caller carrying an id between engines does not learn the difference from a refusal.