Older pak files fail to deserialize when loaded in new version of the project.

Pak files are created (after cooking) using UnrealPak.exe command from within the editor, No Chunking. They are then distributed and loaded at runtime as mods for our game.

[Attachment Removed]

Steps to Reproduce
Have a C++ class that a blueprint inherits from called CharacterData. We are able to cook, pak, and load this class fine within our game.

If we were to make a change to CharacterData like adding a new UPROPERTY then any previous paks no longer load, and get this error during serialization. We are unsure how to not break previously created pak files when updating the game.

LogWindows: Error: appError called: Assertion failed: Index.IsExport() && ExportMap.IsValidIndex(Index.ToExport()) [File:D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Public\UObject\Linker.h] [Line: 165] 

[Attachment Removed]

Hey Joshau,

Unfortunately, this is per design.

Cooked data is serialized as unversioned data that does not support upgrading from older versions, in constrast to the way that migrations in the Editor are handled. Cooked data is assumed to be only compatible with the same Engine version it was built with and won’t be able to deal with migrations or layout changes.

For patch releases (e.g. 5.4.X) we guarantee binary compatibility, but for any major Engine upgrades or changes to your game classes it is quite possible that older data built in a previous version will not load.

If any base classes or other dependencies change their serialized layout it will be necessary to re-cook the mods to stay compatible.

A good practice is to store the engine/game version that a mod was built for in it’s .uplugin file or some separate metadata and then prevent loading it if it is not compatible.

Kind Regards,

Sebastian

[Attachment Removed]

The problem isn’t that we are performing an engine change, the version of the engine will remain the same. The issue is a new build of the game where there is a change to the underlying C++ of a UClass object. As stated in the above response. If I have something like

UCLASS()
class MYGAME_API MyCharacter : public UObject
{
    UPROPERTY()
     FString Name;
}

Then I were to modify it to something like

UCLASS()
class MYGAME_API MyCharacter : public UObject
{
    UPROPERTY()
     FString Name;
 
     UPROPERTY()
     Int32 Level;
}

Then any .pak files that were made with the first iteration of the class will fail to deserialize at runtime and cause a crash. Though my understanding is that it should be able to convert in place and Level = NULL.

[Attachment Removed]

hmmm so if I’m understanding correctly, the best way to handle previously cooked content remains compatible is to make sure any underlying C++ headers don’t have a layout change (no adding or removing fields/methods)? But if we do need to make changes then there isn’t anything we can do for runtime conversion, we just need to notify the modding community for them to re-cook their mods with an updated version of the project?

[Attachment Removed]

I guess then the only other solution is to roll out our own serialization for these classes to maintain compatibility, but we were really hoping the default serialization would cover the changes.

[Attachment Removed]

> if I’m understanding correctly, the best way to handle previously cooked content remains compatible is to make sure any underlying C++ headers don’t have a layout change (no adding or removing fields/methods)? But if we do need to make changes then there isn’t anything we can do for runtime conversion, we just need to notify the modding community for them to re-cook their mods with an updated version of the project?

In general, with the default cook settings, you’re correct. Cooked projects don’t retain the information needed for graceful handling of layout or type changes by default. As such, a packaged game can’t upgrade old data the way the Editor does.

We’re aware this makes modding and UGC in general more complicated. These choices where in the early days of the Engine with a focus on the best loading performance at runtime.

You can, however, try to cook with versioned content, to use the same serialization as in Editor.

This will affect performance and this path is not exercised much by us or other licensees. I can’t promise that it will actually work, but it might be worth a try to increase your mod compatibility at the risk of using a fairly niche feature that might have other lingering issues.

To enable versioned cooks you’ll need to add the -VersionCookedContent flag to UAT’s BuildCookRun (instead of -UnversionedCookedContent which is the default). Alternatively you can remove the -unversioned flag from the CookCommandlet invocation directly (UAT just hands things through to the commandlet).

You’ll need to compile both the game and the mods with that option.

Kind Regards,

Sebastian

[Attachment Removed]

hmmmm we were cooking the mods with the following command FString Command = TEXT( “../../../Project/Game.uproject -run=cook -targetplatform=Windows -iterate -stdout -unattended -UTF8Output” ); So unfortunately that didn’t really seem to solve our problem. I am hoping that I can just override the FArchive* operator<<() function to allow us to handle the serialization. But in my early tests it doesn’t look like we are going into the function call. Do you know if perhaps SerializeDefaultObject uses a different serializer call? So far I have tried the following functions, but have not hit them at runtime.

virtual void Serialize(FArchive& Ar) override;

friend FArchive& operator <<(FArchive& Ar, UCharacterData& CharData);

[Attachment Removed]

Also for more context into the problem we are bumping up against when loading older pak files, here is our specific call stack

[Inline Frame] Rivals2.exe!FLinkerTables::Exp(FPackageIndex) Line 165
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Public\UObject\Linker.h(165)
[Inline Frame] Rivals2.exe!FLinkerLoad::ResolveResource(FPackageIndex) Line 4286
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\UObject\LinkerLoad.cpp(4286)
Rivals2.exe!FLinkerLoad::operator<<(UObject * & Object) Line 6166
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\UObject\LinkerLoad.cpp(6166)
Rivals2.exe!FArchiveProxy::operator<<(UObject * & Value) Line 46
	at D:\Rivals2Snapnet\Engine\Source\Runtime\Core\Public\Serialization\ArchiveProxy.h(46)
[Inline Frame] Rivals2.exe!FBinaryArchiveFormatter::Serialize(UObject * &) Line 273
	at D:\Rivals2Snapnet\Engine\Source\Runtime\Core\Public\Serialization\Formatters\BinaryArchiveFormatter.h(273)
[Inline Frame] Rivals2.exe!FStructuredArchiveSlot::operator<<(UObject * &) Line 349
	at D:\Rivals2Snapnet\Engine\Source\Runtime\Core\Public\Serialization\StructuredArchiveSlots.h(349)
Rivals2.exe!FObjectProperty::SerializeItem(FStructuredArchiveSlot Slot, void * Value, const void * Defaults) Line 227
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\UObject\PropertyObject.cpp(227)
[Inline Frame] Rivals2.exe!FUnversionedPropertySerializer::Serialize(FStructuredArchiveSlot) Line 114
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\UnversionedPropertySerialization.cpp(114)
Rivals2.exe!SerializeUnversionedProperties(const UStruct * Struct, FStructuredArchiveSlot Slot, unsigned char * Data, UStruct * DefaultsStruct, unsigned char * DefaultsData) Line 909
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\UnversionedPropertySerialization.cpp(909)
Rivals2.exe!UStruct::SerializeTaggedProperties(FStructuredArchiveSlot Slot, unsigned char * Data, UStruct * DefaultsStruct, unsigned char * Defaults, const UObject * BreakRecursionIfFullyLoad) Line 1328
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\UObject\Class.cpp(1328)
Rivals2.exe!UClass::SerializeDefaultObject(UObject * Object, FStructuredArchiveSlot Slot) Line 5583
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\UObject\Class.cpp(5583)
Rivals2.exe!UBlueprintGeneratedClass::SerializeDefaultObject(UObject * Object, FStructuredArchiveSlot Slot) Line 735
	at D:\Rivals2Snapnet\Engine\Source\Runtime\Engine\Private\BlueprintGeneratedClass.cpp(735)
[Inline Frame] Rivals2.exe!UClass::SerializeDefaultObject(UObject *) Line 3410
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Public\UObject\Class.h(3410)
Rivals2.exe!FAsyncPackage::EventDrivenSerializeExport(int LocalExportIndex) Line 3534
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoading.cpp(3534)
Rivals2.exe!FAsyncPackage::ProcessImportsAndExports_Event() Line 3853
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoading.cpp(3853)
Rivals2.exe!FAsyncPackage::Event_ProcessImportsAndExports() Line 2912
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoading.cpp(2912)
Rivals2.exe!FAsyncLoadingThread::QueueEvent_ProcessImportsAndExports::__l2::<lambda>(FAsyncLoadEventArgs & Args) Line 2689
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoading.cpp(2689)
[Inline Frame] Rivals2.exe!UE::Core::Private::Function::TFunctionRefBase<UE::Core::Private::Function::TFunctionStorage<0>,void __cdecl(FAsyncLoadEventArgs &)>::operator()(FAsyncLoadEventArgs &) Line 555
	at D:\Rivals2Snapnet\Engine\Source\Runtime\Core\Public\Templates\Function.h(555)
[Inline Frame] Rivals2.exe!FAsyncLoadEventQueue::PopAndExecute(FAsyncLoadEventArgs &) Line 108
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoadingThread.h(108)
Rivals2.exe!FAsyncLoadingThread::ProcessAsyncLoading(int & OutPackagesProcessed, bool bUseTimeLimit, bool bUseFullTimeLimit, float TimeLimit, FFlushRequest & FlushRequest) Line 4393
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoading.cpp(4393)
Rivals2.exe!FAsyncLoadingThread::TickAsyncThread(bool bUseTimeLimit, bool bUseFullTimeLimit, double TimeLimit, bool & bDidSomething, FFlushRequest & FlushRequest) Line 5300
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoading.cpp(5300)
Rivals2.exe!FAsyncLoadingThread::Run() Line 5225
	at D:\Rivals2Snapnet\Engine\Source\Runtime\CoreUObject\Private\Serialization\AsyncLoading.cpp(5225)
Rivals2.exe!FRunnableThreadWin::Run() Line 149
	at D:\Rivals2Snapnet\Engine\Source\Runtime\Core\Private\Windows\WindowsRunnableThread.cpp(149)
Rivals2.exe!FRunnableThreadWin::GuardedRun() Line 71
	at D:\Rivals2Snapnet\Engine\Source\Runtime\Core\Private\Windows\WindowsRunnableThread.cpp(71)

[Attachment Removed]

Hey Joshua,

apologies for the wait. I’ll loop in one of the developers on the loading team, they might be able to provide you a bit more background info.

Please be aware that making the unversioned serialization support versioning after the fact is a bit too much of an engine modification for us to be able to provide you support with it, if you really want to go this route you’d be mostly on your own.

[Attachment Removed]

Thanks for the update. It looks like an answer has been selected as best, and you’re on another project at this point. Is there anything else you need support with on this topic, or are we good to close up this ticket?

[Attachment Removed]

Unfortunately the -VersionCookedContent flag did not resolve this issue we are seeing.

.\RunUAT.bat BuildCookRun -project="$ENV:ROA2Workspace\Game\Rivals2.uproject" -noP4 -platform="Win64" -target=Rivals2 -clientconfig="Development" -serverconfig="Development" -cook -VersionCookedContent -allmaps -build -stage -pak -archive -archivedirectory="$ENV:ROA2Workspace\Tools\Upload\Platforms"We also have always had the -Unversioned flag removed from our cooking of mods. I am a bit skeptical that the build did build with version content, as we are still seeing a call stack that is calling FUnversionedPropertySerializer::Serialize().

[Attachment Removed]

The above issue is more pressing, as this is what is actually causing our crash. The ResolveResource function tries to access an invalid index (usually 3 or 4) in the linker ExportMap (which never has more than 2 entries, CharacterData_C and Default_CharacterData_C).

[Attachment Removed]

I am actually no longer on the project that had this issue. However, the last direction we were going was to instead use JSON to repopulate a new version of the classes that way we don’t have to worry about versioning. A bit more work than we were hoping for. If I may make a suggestion perhaps add more clarifying info about the versioning to existing documentation. The way that I had interpreted it was that pak files would be able to handle deserialization as long as the Engine version remained the same, but that seems to only be an Editor specific feature.

[Attachment Removed]