Some Item in TArray<TSoftClassPtr<UObject>> set to null for non obvious reason in GameSave

Hello,

Got a bug on production triggered time to time and i can’t find any clue about it :

We have a custom USaveGame with this specific field

  UPROPERTY(BlueprintReadOnly)
  TArray<TSoftClassPtr<UObject>> Items;

Some users report the loss of some items. By inspecting their GameSave, i saw that some SoftClassPtr were serialized as “None” at some point.

Problem :

there’s no line in code where we set or reset Items

Only Items.AddUnique() with non null softclass and Items.Contains functions are present in our whole codebase.

I dig a lot into serialization and can’t find anything that could provoke an overwriting like this.

My first hypothesis was about having issue items not being loaded yet, or object renamed … but that do not affect serialization (right ?)

I thought about type change, but only new field appears in this game save (and even if order does not affect archive loading, order was not changed, all new fields were added after this one.

And also, this bug seems totally random and can appear at some point, without any update or stuff like this.

I’m thinking about changing type to serialize the path as FString and do the path creation on our side, but maybe this bug is not about SoftClassPtr.

I tried to find some related stuff into support/forum/bug tracker without any success.

Hi!

TSoftClassPtr is basically a glorified FName with some helper functions and checks; it should serialize just fine. I don’t see a problem with your implementation, except that you’re using a generic base class (UObject) as the template, rather than a more specific one tied to your game, such as UInventoryItem (name made up).

The reason that stands out to me is that I have a suspicion: consider that TSoftClassPtr is designed specifically for instantiateable classes, NOT for Data Assets. It has some extra functionality to make sure that Blueprint classes are handled correctly (pointing to the UBlueprintGeneratedClass, ending with “_C”, instead of the Blueprint class itself, which only exists in Editor time).

TSoftObjectPtr should instead be used for Data Assets, as they are never instantiated. Might that be your problem?

Thanks for the quick answer,

I agree on the specialisation, think the original developement wanted to maximise possibility.

Just checked, it could only be specialized on AActor at the moment (but i could implement a stub for this type of object)

So unfortunately, this do not point to DataAsset (i also check about this issue after searching on bugtracker / support)

i double checked valid save :

/Game/Darkhours/Actors/MC/Custos/Blueprints/Hats/BP_CustoH_Propeller_01 BP_CustoH_Propeller_01_C LSo the pointed class seems legit.

Also, time to time i got report with entire array begin gone, but also one item set to none in a middle of a 30+ array.

Still really lost here, changing to AActor/ Custom Class could make a diff here ?

sorry if my explanation are not clear enough, it’s not a question of resolve / load (or is it? )

And if i’m not mistaken, the TSoftClassPtr internal (specifically AssetPath) should never be impacted by resolution or loading.

And to be precise, we never resolve this array, the only code using this array is :

//loop over a list of "Buyable" object
//Buyable object has a buyableClass member  of type TSoftClassPtr<UObject>
for(auto& Buyable : Buyables) 
{
       //settingsSubsystem : a GameInstance Subsystem loading gameplay save
      // the gameplay save contains the array of soft ptr
	if (settingsSubsystem->pGameplaySave->Items.Contains(Buyable->buyableClass))
	{
	     AllBuyableArray.Add(Buyable);
	}
}

But i also suspected that (maybe at some point, someone did the resolution then removed the code later)

In fact my first check was to completely delete an asset referenced in a save, then loading that save.

Got no overwriting of the internal path to None. (was in editor, but checked about code specific and nothing seems to be different in shipping)

i’m currently looking at possible data race since the only weird behavior i can see clearly right now, is loading/saving this save multiple time on a frame. (well to be precise : multiple call to Serialize)

don’t think so :

using

UGameplayStatics::LoadGameFromSlot(gameplayDataName, 0)); and

UGameplayStatics::SaveGameToSlot(pGameplaySave, gameplayDataName, 0);

will check that, i did not go in that direction.

but 26 save and one load in the middle seems sus’

Since no one got issue with serialization + your feedback on this matter : Really seems to me that error should be somewhere on our side anyway.

I’m suspecting the same. I would add some guard code around the call to save the game, which would iterate through the array and assert if there are any None properties to try to identify the source of the issue.

Added All the checks (check to none, but also check if all is done in the gamethread).

Removed all the unnecessary call to Save.

Will wait for another user report.

Thanks by the way, we can close this thread, i will refer to it if it come back.

Ok, so they’re serializing correctly. And only turn to null when you try to resolve them? They might just not be loaded.

When you try to resolve a soft class pointer, it will only resolve to a class if that class is already loaded. Otherwise, it will resolve to null. That’s on purpose, as the whole point of soft pointers is not to keep things loaded until you explicitly load them. You’ll need to load the class before you can instantiate it.

[Image Removed](For EDC: a picture of a soft class pointer being resolved, with a comment saying “this will be NULL if the class isn’t already loaded”)

This is how you should use it:

[Image Removed](For EDC: a picture of a soft pointer being a resolved, and then checked for IsValid to see if it’s loaded. If not, the Async Load Class Asset function is run to load it)

I see. No, you’re right, that’s not a question of a resolve/load.

You have a clear and simple usage that I see no reason for it to fail :thinking:

It should get serialized to an array of strings (basically). Saving and loading that should just work.

I don’t think calling SaveGame multiple times should make a difference, it will immediately serialize the object to a byte array and finish the save before returning from the function. Are you maybe calling the Async version of SaveGame multiple times and the threads are overriding each other?