Binary Compressed Save System, Save Entire Dynamically Generated Worlds! ♥ Rama

Hi @Rama,

We’ve been using this plugin for a while, and it’s been working great!

Today we upgraded to UE4.21 and tried to use the new **ARamaSaveEngine::**LoadProcessFinished(const FString& FileName) by creating a child of ARamaSaveEngine as a blueprint and setting the appropriate field in the URamaSaveSystemSettings configuration, however this seems to cause a “circular dependency” between the Plugin and the Game Module when loading the project, causing it to crash at startup, or cause other modules to fail loading (usually other plugins).

No amount of tweaking of the Additional Dependencies and Loading Phase of both the Project and the Plugin was able to resolve this, so we’re currently unable to use the feature (custom Rama Save Engine Classes) at all.

I would have liked to offer up a solution, but this seems to me a pretty fundamental problem?

I and Justin.Dooley have the same issue. As soon as we override the class in blueprint, referencing other blueprint assets within and set it in the RamaSave settings, we can’t open the project anymore. You can open the project again when removing the custom save engine class from the config file.
See here.

@Justin.Dooley
Did you make any progress on this?

For now I copied the plugin into the project, commented the Settings property for the save class and forcing to use the custom class.

in RamaSaveLibrary::URamaSaveLibrary::GetOrCreateRamaEngine

Replace



    TSubclassOf<ARamaSaveEngine> RamaEngineClass = Settings->RamaSaveEngineBP;
    if (!RamaEngineClass)
    {
        RS_LOG(RamaSave, "Rama Save Engine class in RamaSaveSystemSettings was invalid!!!");
        RamaEngineClass = ARamaSaveEngine::StaticClass();
    }


With



UClass* RamaEngineClass = LoadObject<UClass>(nullptr, TEXT("BlueprintGeneratedClass'/Game/PaintWorld/Blueprints/Interface/SaveLoad/BP_RamaSaveEngine.BP_RamaSaveEngine_C'"));
check(RamaEngineClass);


@Rumbleball It’s good to hear I’m not alone!
I have been (and have done again) changed the base RamaSaveEngine class to have multicast delegates for all the relevant events, and added some static functions to the Library for adding bindings to them.

does it solve the issue for you? You should be aware that the RamaSaveEngine only lives during save process and gets destroyed afterwards. You might want to have the delegates exist in your PlayerController and let the save engine trigger them. This way you can bind at anytime and do not loose the bindings after save

@Rumbleball It was 100% my own fault and not the plugins. I created a circular dependency with my widget. This came because I had my hud class displaying a progression bar for async saving. To fix it, I had to break a link in dependency.

I have multiple circular dependecies, like the player needs the game instance and the game instance needs the player. But it works. Dunno why it causes so much issues with the save engine if loaded from config.

Well having any circular reference should be avoided. It can work but is not optimal. A few things that can go wrong is:

  • You can’t use static linking
  • cant use dependency injection
  • Can cause simple recursive algorithms to crash. (serialization might have this issue in UE4 I haven’t dug too deep into it)
  • If you do have a working circular reference and you need to recompile a class it increases compile time because it forces you to recompile both classes
  • They are generally confusing when debugging.

Essentially just avoid them if you can and the benefits speak for themselves. I remember Epic was battling Blueprint circular links back around 4.12 or something. Might want to look into what they did there.

You are talking about C++? I was talking about Blueprint. Dunno if thats a difference in this case. C++ should not compile at all if there is a circular dependency in headers.

It does, as far as I can tell once an ARamaSaveEngine has spawned it sticks around until the current persistent level is unloaded. It’s a bit strange we’ve had different experiences with this?

Here’s what I do: add two dynamic delegate declarations above ARamaSaveEngine



DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSimpleDynamicMultiDelegate);
DECLARE_DYNAMIC_DELEGATE(FSimpleDynamicDelegate);

UCLASS()
class ARamaSaveEngine: public AActor


I then add a UPROPERTY for the Multicast delegate and a static UFUNCTION for binding to that delegate from anywhere in BP into the class:



private:

UPROPERTY()
        FSimpleDynamicMultiDelegate OnLoadProcessFinished;

public:

UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject"))
        static void AddOnLoadProcessFinished(UObject* WorldContextObject, FSimpleDynamicDelegate InEvent);


Function body:



void ARamaSaveEngine::AddOnLoadProcessFinished(UObject* WorldContextObject, FSimpleDynamicDelegate InEvent)
{
    check(WorldContextObject);
    ARamaSaveEngine * engine = URamaSaveLibrary::GetOrCreateRamaEngine(WorldContextObject->GetWorld());
    engine->OnLoadProcessFinished.Add(InEvent);
}


And then, immediately below the call to:



LoadProcessFinished(LoadParams.FileName);
OnLoadProcessFinished.Broadcast();

I put the broadcast in for the Multicast delegate. It should be relatively simple to replicate this for all the other events, but I don’'t have a need for those.

Hm, can’t find anything about destroy. Maybe I’m completly wrong, or Rama changed it at some point.

Hoping to get some help form a resident Rama Save System master or @Rama himself! I’ve got a USTRUCT defined like this:


USTRUCT(BlueprintType)
struct FDeathSplatter
{
    GENERATED_USTRUCT_BODY()

    // The decal associated w/ this death splatter
    UPROPERTY(Transient)
    UDecalComponent* Decal = nullptr;

    // The decal material associated w/ this death splatter
    UPROPERTY(SaveGame, Transient)
    UMaterialInterface* DecalMaterial = nullptr;

    // The world transform for the decal
    UPROPERTY(SaveGame, Transient)
    FTransform DecalTransform;

    // Size of the decal
    UPROPERTY(SaveGame, Transient)
    FVector DecalSize = FVector::ZeroVector;

    // Size of death splatter on screen when they start fading (pct of screen???)
    UPROPERTY(SaveGame, Transient)
    float DecalFadeScreenSize = -1.f;

    FDeathSplatter()
    {

    };
};

Then, I’ve got a static RamaSaveObject with an array of those USTRUCTs defined like this:


UCLASS()
class PRIZEFIGHTER_API UMissionSaveObject : public URamaSaveObject
{
    GENERATED_BODY()

public:

    // All the death splatter in the world
    UPROPERTY(SaveGame, Transient)
    TArray<FDeathSplatter> DeathSplatterList;
};


I don’t get any error messages upon loading or saving, but when I load my MissionSaveObject comes back with the correct number of array members…but the values in the USTRUCTs are all set to default :confused:

Is it possible that the save system doesn’t work with certain types of USTRUCTs? Anyone notice something else I’m doing wrong here? I have another instance where saving/loading a USTRUCT works just fine, so no great leads at the moment.

Thanks!

Managed to figure it out! Those “Transient” UPROPERTY tags seem to have been the problem :confused: Since the Rama Save System works off the “SaveGame” tags, I didn’t anticipate them having any effect. Oh, well! They were totally unnecessary so just removed them. Good to know and easy fix.

The transient flag, as far as I know, is for assets placed in the world. When saving within the editor, that property is not saved for instances of that class/struct.

Exactly. That’s how I’ve used it. Was strange to see it have an effect on the Rama Save System.

Rama has done this already and it works for Synchronous save. Though, as I just noticed, this is still buggy for Asyncronous saving.

guys how to destroy actor with GUID?

never worked with the guid stuff. Can you describe your scenario? Maybe we can work around

Setting a GUID on the save component is explicitly for scenarios when you don’t want the actor destroyed/recreated upon loading from a save. The benefit there is you can maintain valid pointers to that actor in the world after loading a save.

I’d suggest reviewing Rama’s tutorial videos associated with this plugin. Most of my questions have been answered there :slight_smile:

Hey, @Rama! I found and fixed a bug in the ASYNC code :slight_smile: I updated ARamaSaveEngine::RamaSave_SaveToFile_ASYNC so that it mimicked the same writing sequencing as ARamaSaveEngine::RamaSave_SaveToFile where it creates a FObjectAndNameAsStringProxyArchive after versioning. Was crashing trying to save static data with object references when using the ASYNC system only. This turned out to be the issue.


    
    FMemoryWriter MemoryWriter(NewUnit.ToBinary, false);

    //~~~ Versioning ~~~

    //! #1

    // Write version for this file format
    int32 SavegameFileVersion = JOY_SAVE_VERSION;
    MemoryWriter << SavegameFileVersion;        //<~~~ Rama Custom Serialization Version

    //! #2

    // Write out engine and UE4 version information
    int32 PackageFileUE4Version = GPackageFileUE4Version;
    MemoryWriter << PackageFileUE4Version;
    FEngineVersion SavedEngineVersion = FEngineVersion::Current();
    MemoryWriter << SavedEngineVersion;

    //~~~ End Versioning ~~~

    //Obj and Name as String
    FObjectAndNameAsStringProxyArchive Ar(MemoryWriter, false);

    //~~~