Hi there!
I’ve implemented a custom UPrimaryDataAsset class that has some properties that I aim to serialize specially, separately from the actual .uasset on disk, these sometimes represent references to other data assets on disk. Specifically, I’m trying to save these properties (TObjectPtr<>s) as separate .json files on disk with asset paths to the referenced assets. These assets are never cooked, editor only.
I’ve built a little experiment where I mark those properties (TObjectPtr<>s) as Transient so they don’t get written out or loaded automatically from the saved asset, then in PreSave() and PostLoad() I have code to respectively save the data I care about separately from the .uasset on disk and loading it from that separate file (.json text) into the data asset on load. That all seems to work fine.
One thing I haven’t yet gotten to work is proper UE understanding of asset references.
If I just save the references as normal in the data .uasset, the reference viewer can show them and I can see referenced assets. However, if I go through these PreSave() and PostLoad() tricks dumping and filling the Transient properties it doesn’t seem like the registry is aware of these package references and they won’t show in the reference viewer.
How would you recommend to go about this? Should I use other overrides instead of PreSave() and PostLoad()?
Cheers,
Daniele
I think I was able to get this to work by using
ObjectSaveContext.AddCookBuildDependency(UE::Cook::FCookDependency::Package(Obj->GetPackage()->GetFName()));In my custom pre-save.
But, I would still like any good note that you have about doing what I’m trying to do and gotchas I should be aware of!
Declaring them in your c++ hooks called during SavePackage is correct, but you need to serialize them as FSoftObjectPath in your Serialize function rather them calling AddCookBuildDependency during your PreSave function
During editor saves (non-cook saves) CookBuildDependency has a special meaning: the dependency is not a runtime dependency, but still marks which chunk the dependency should go into.
During cook saves, CookBuildDependency has a different special meaning: the dependency is an incremental cook dependency. This means that during incremental cooks that cook only what will change when recooked, when the target changes, the source holding the dependency is also recooked.
Since you want these to be normal runtime dependencies, so that they are added to the cook output when the source holding the dependency is cooked, and put into the same chunk as the source holding the dependency, and you want them to be visible in the reference viewer, you should mark them during editor save as used-in-game SoftPackageReferences. SoftPackageReferences show up in the reference viewer (you have to select Show Soft References) and cause a package to be cooked and added to the chunk of its referencer whenever the referencer is cooked. They are soft rather than hard because you are not relying on the linker to load them for you automatically when you load the package, and instead you load them yourself from one of your c++ runtime hooks.
We don’t have a way to declare SoftPackageReferences during PreSave at the moment, you will have to use Serialize. Override UObject::Serialize if you are not already doing that (be sure to call Super::Serialize), and if and only if the FArchive reports that it is both a SavePackage archive and an Object Collector, serialize the references you have as SoftObjectPath:
`void UMyClass::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
// Only do this when saving, it’s not a problem to do it otherwise but is unused and wasteful.
if (Ar.GetSavePackageSerializeContext() != nullptr)
{
// Only do this when the SavePackage archive being used is the ObjectCollector archive
// Serializing when not an ObjectCollector archive will affect the bytes of the UObject written to
// disk, which is incorrect behavior for just declaring that your package has these references.
if (Ar.IsObjectReferenceCollector())
{
FSoftObjectPath Reference = ConvertMyObjectReferenceIntoASoftObjectPath();
Ar << Reference;
}
}
}`
Thanks Matt!
Makes sense.
One quick thing: I actually don’t need these to be runtime dependencies, since these assets are editor only and they will never be cooked (they are consumed by our commandlets and by the cook itself though).
Would you still recommend using the Serialize() override approach above? Or does that change your recommendation?
Cheers,
Daniele
Yes, even if the references are editor only, the Serialize approach is the best case. To mark them as editoronly references, you should only do them if (!Ar.IsCooking()), and you need to add an editor-only scope around them:
`void UMyClass::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
// Only do this when saving and not cooking, it’s not a problem to do it otherwise but is unused and wasteful.
if (Ar.GetSavePackageSerializeContext() != nullptr && Ar.IsCooking())
{
// Only do this when the SavePackage archive being used is the ObjectCollector archive
// Serializing when not an ObjectCollector archive will affect the bytes of the UObject written to
// disk, which is incorrect behavior for just declaring that your package has these references.
if (Ar.IsObjectReferenceCollector())
{
FSoftObjectPath Reference = ConvertMyObjectReferenceIntoASoftObjectPath();
// Mark that the reference is editor-only
FSoftObjectPathSerializationScope SerializationScope(NAME_None, NAME_None, ESoftObjectPathCollectType::EditorOnlyCollect, ESoftObjectPathSerializeType::AlwaysSerialize);
// Serialize the reference into the object collector to record it in the AssetRegistry dependencies
Ar << Reference;
}
}
}`