The config ini changes in UE 5.5: Save, Load, Plugin

Conclusion

  • In the case of the plugin
    • Make a Config folder under the plugin folder, and make Default{PluginName}.ini in there.
      • In plugins, we can use only the name of the plugin as the ini file name.
      • Default{PluginName}.ini can be empty. It doesn’t need any special content.
      • Use the PluginName as the value of the specifier Config in the UClass.
  • In the case of the module
    • To make possible to load
      • You can see the Config folder under your project folder. The Default ini file should exist there. If it does not, create one.
    • To make possible to save
      • The section [SectionsToSave] should exist in the Default ini file. If it does not, add it.
      • Add bCanSaveAllSections=true in the section [SectionsToSave]. There is another way that you can specify explicitly which classes are allowed to be saved.
  • You can use the “known” categories as the file name of ini file. But in this case, you should set the Default ini file to make it possible to save classes that belong to the plugin.

Preface

Official Documents

Wiki

Unreal community wiki: Config Files, Read & Write to Config Files

Version

As the title suggests, this document is based on Unreal Engine version 5.5.

Goal

This document assumes that we are working only with specifiers of UCLASS and text ini files, without any additional c++ code.

That is, there is only one function we will call at the c++ level, UObject::LoadConfig GitHub Link .

This is because I want code and resources that can work well on older versions of the engine.

However, in a detailed explanation, there will likely be mentions of various aspects of the engine source code.

Terms

In the engine code, it is also called an ini file or a Config file, but from now on, I will call it an ini file.

Sample Project

I uploaded an example project about this topic. Link

I created this project to use when I want to know the detailed behavior of the engine through debugging.

Long long time ago

Until 5.3

1
2
3
4
5
6
UCLASS(Config = SomeConfigFileName)
class SOMEPROJECTNAME_API USomeClassName : public UObject
{
  UPROPERTY(Config)
  int32 ValueInObject;
}

This will create a file name of SomeConfigFileName.ini, and the Config values will be saved in it. In the example above, the value of ValueInObject will be saved.

In 5.4

I wrote a post about some limitations in customizing ini file names with config specifiers in UCLASS in 5.4.

However, in 5.5, we can’t use the method of adding User or Editor to the ini file name. If you do this, it will save but not load in 5.5.

Since UE5.5

It depends on whether it is an ini file for a module or an ini file for a plugin.

If you are not familiar with the words module or plugin, you can think of a module as something other than a plugin and just look at the module part.

In the case of the module

When using the “known” categories(=file name), which the engine provides

“known” categories(=file name)?
  • Example: Engine, Game, Input
  • Please see this page for the entire list.
How to
  1. Check if there is already a Default ini file in the Config folder directly under the project folder. If not, you will need to create it using Notepad or something similar.
    • The file name is Default{CategoryName}.ini.
      • For example, if you use the Game category, it is DefaultGame.ini.
  2. Check if the ini file has a [SectionsToSave] section. If not, add it. Most likely, it won’t.
  3. Add bCanSaveAllSections=trueinto the section [SectionsToSave].
    • The reason for adding this is that, for modules, the default value of bCanSaveAllSections is false. To allow saving of all classes, we need to change this to true. I will explain in detail in the separate chapter below.

The result will look like this:

1
2
[SectionsToSave]
bCanSaveAllSections=true

The cpp code would look like this. Let’s assume we’re using the Game category. If you’ve used Config in previous versions, this should look familiar.

1
2
3
UCLASS(Config = Game)
class SOMEPROJECT_API USomeClassAlpha : public UObject
{...}

If you want to use a file name other than the categories provided by the engine

Compared to the case in the previous chapter, the only difference is that you always have to add a Default ini file.

  1. Create a Default ini file in the Config folder right under the project folder.
    • If the file name you want to use is MyConfigCategory.ini, create it as DefaultMyConfigCategory.ini.
  2. Open the file and add the section [SectionsToSave] and bCanSaveAllSections=true.

The result will look like this.

1
2
[SectionsToSave]
bCanSaveAllSections=true

The cpp code would look like this.

1
2
3
UCLASS(Config = MyConfigCategory)
class SOMEPROJECT_API USomeClassAlpha : public UObject
{...}

In the case of the plugin

  1. Create a Config folder in the plugin folder.
  2. Create Default{plugin name}.ini in that folder.
    • For example, if the plugin name is MyPlugin, it would be DefaultMyPlugin.ini.

For plugins, this is all. It’s ok even if the ini file is empty.

For plugins, unlike modules, even if there is no [SectionsToSave] section and bCanSaveAllSections=true, all classes are saved and loaded.

This is because the default value of bCanSaveAllSections for plugins is true, which is different from modules. I will explain the details in a separate chapter below.

So, what you need to do is just make a file named Default{plugin name}.ini in your Config folder.

Instead, there is a different limitation in a plugin. Currently, without additional c++ code, you can not use an ini file name that is neither a “known” category nor the name of the plugin in a plugin.

The use of the UCLASS specifier is no different, except that only the name of the plugin can be used, as mentioned. It would look like this:

1
2
3
UCLASS(Config = MyPlugin)
class SOMEPROJECT_API USomeClassAlpha : public UObject
{...}

Advanced: Adding which classes are allowed to be saved to the ini file explicitly

You can explicitly add classes that you want to allow to be saved to the [SectionsToSave] section of the ini file. This way, even if the current value of bCanSaveAllSections is false, the class will be saved as a Config.

However, when add the class name, you must add it without the prefix U!

For example, if you want to add only USomeClass belonging to module MyModuleName to the ini, you can do it like this:

1
2
[SectionsToSave]
+Section=/Script/MyModuleName.SomeClass

You can also add classes to plugins. However, in the case of plugins, the default value of bCanSaveAllSections is true, which is the opposite of the module, so if you want to prevent saving classes other than the specified class, you must also add bCanSaveAllSections=false to [SectionsToSave].

For example, as shown below. Also, note that the prefix U is removed from the class name.

1
2
3
[SectionsToSave]
bCanSaveAllSections=false
+Section=/Script/MyPluginName.SomeClass

Additional explanation for those who are not familiar with it

  • If you don’t know what a module is, for this chapter only, you can think of it as the project name.
  • Additional explanation for the added +Section=/Script/MyPluginName.SomeClass part
    • As written in the official documentation, the + symbol in front of Section means that it is added to an array.
    • The /Script/ part, very roughly speaking, means that the content is about c++ code.
    • The name following /Script/ is the module name or plugin name.
    • The name of the class follows after that, without the U prefix.

Details

Rough Summary

I’ll write a very rough summary of what I’ll cover in this chapter. Please keep in mind that this summary may not always be correct, as there are many small details and exceptions.

  • Load
  • Save
    • The ini file of the module inherits /Engine/Config/Base.ini. The value of bCanSaveAllSections inside [SectionsToSave] of this file is false.
    • The ini file of the plugin inherits /Engine/Config/Base.ini. The value of bCanSaveAllSections inside [SectionsToSave] of this file is true.
    • When the value of bCanSaveAllSections is false in the section [SectionsToSave], if you did not specify which class will be saved explicitly, it will not be saved.
    • When the value of bCanSaveAllSections is true in the section [SectionsToSave], all classes will be saved.

Concept and structure which you should know

BaseIniName and several types of ini files

Default ini file

When you create a new project in Unreal Engine, the following files are created under the Config folder in your project folder.

  • DefaultEditor.ini
  • DefaultEngine.ini
  • DefaultGame.ini
  • DefaultInput.ini

I will call these as Default ini files from now on.

BaseIniName?

Here, the parts excluding the Default part from Default ini files, that is, the Engine, Game, Input… parts are called “categories” in the official documentation. In the code, ‘‘‘BaseIniName’’’ is used as a variable or parameter name. Because our interest in this post is the file name, we will use the term BaseIniName in this post.

Final ini file

If you’ve created a project and have ever turned the editor on and off,

You’ll also find a few ini files in your project folder in the /Saved/Config/WindowsEditor/ folder, like this:

  • Editor.ini
  • EditorPerProjectUserSettings.ini
  • GameUserSettings.ini

The filename here is missing the Default part, and just BaseIniName.

This is where your data exists, both saved and loaded. Let’s call this Final ini file.

Location of Final ini file when packaging

If you packaged your project, the path to the Final ini file will be as follows.

If not Shipping, if you ran it from C:/MyPackageFolder/

1
C:/MyPackageFolder/Saved/Config/Windows/MyIniNameInModule.ini

If it’s the Shipping version

1
C:/Users/MyUserName/AppData/Local/MyProjectName/Saved/Config/Windows/MyIniNameInModule.ini

Also, if you look at the engine source code, you’ll see variable names like FinalCombinedLayers, which are completely different from the Final ini file, so don’t get confused.

Base ini file

In fact, there are more ini files in the Unreal Engine folder.

  • If you are using the launcher version,
  • If you haven’t changed the engine installation folder,

You can see a lot of ini files in the C:/Program Files/Epic Games/UE_5.5/Engine/Config folder. (In my case, 34.)

There is also BaseEditor.ini. It has Base in front of Editor.ini.

I will call this kind of thing Base ini file from now on.

It is different from BaseIniName. Base ini file is an ini file with the string Base added in front of BaseIniName.

(I know. Around here, it is a bit confusing.)

Hierarchy of ini file

How ini files are combined, in rough

In short, to make the content of the file where BaseIniName is Editor and final ini file is Editor.ini, the following files are involved.

The engine reads the above files in order and combines them to create the result about BaseIniName Editor.

If there are different values ​​for the same item, the later value will overwrite the previous value.

(Note that not all values ​​are saved, only the contents that have changed from the default value of ClassDefaultObject are saved.)

But that’s not all.

Hierarchy of ini file - in the case of the module

If we list the places where ini files are read through debugging, this is what we get:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[0] = L"../../../Engine/Config/Base.ini"
[1] = L"../../../Engine/Config/BaseMyIniNameInModule.ini"
[2] = L"../../../Engine/Restricted/NotForLicensees/Config/BaseMyIniNameInModule.ini"
[3] = L"../../../Engine/Restricted/NoRedist/Config/BaseMyIniNameInModule.ini"
[4] = L"../../../Engine/Restricted/LimitedAccess/Config/BaseMyIniNameInModule.ini"
[5] = L"../../../Engine/Config/VulkanPC/BaseVulkanPCMyIniNameInModule.ini"
[6] = L"../../../Engine/Config/Windows/BaseWindowsMyIniNameInModule.ini"
...
[19] = L"../../../Engine/Restricted/LimitedAccess/Platforms/VulkanPC/Config/BaseVulkanPCMyIniNameInModule.ini"
[20] = L"../../../Engine/Restricted/LimitedAccess/Platforms/Windows/Config/BaseWindowsMyIniNameInModule.ini"
[21] = L"E:/GitHubDesktop/MyProjectName/Config/DefaultMyIniNameInModule.ini"
[22] = L"E:/GitHubDesktop/MyProjectName/Restricted/NotForLicensees/Config/DefaultMyIniNameInModule.ini"
[23] = L"E:/GitHubDesktop/MyProjectName/Restricted/NoRedist/Config/DefaultMyIniNameInModule.ini"
...
[75] = L"E:/GitHubDesktop/MyProjectName/Restricted/LimitedAccess/Platforms/VulkanPC/Config/GeneratedVulkanPCMyIniNameInModule.ini"
[76] = L"E:/GitHubDesktop/MyProjectName/Restricted/LimitedAccess/Platforms/Windows/Config/GeneratedWindowsMyIniNameInModule.ini"
[77] = L"C:/Users/MyUserName/AppData/Local/Unreal Engine/Engine/Config/UserMyIniNameInModule.ini"
[78] = L"C:/Users/MyUserName/OneDrive/문서/Unreal Engine/Engine/Config/UserMyIniNameInModule.ini"
[79] = L"E:/GitHubDesktop/MyProjectName/Config/UserMyIniNameInModule.ini"

It reads from 80 locations.

In addition to the engine’s defaults, the project’s defaults, and the project’s saves, we can also change the contents of the ini file depending on your platform. Also, we can see some items that lead us to guess that it might be changed depending on the license of the engine. Additionally, we can see several items that look like some intermediate files of the engine.

If you want to see the source code for the part where this is created, see this footnote. 1

Hierarchy?

The variable name in the engine for the list in the example above is Hierarchy. So in this post, we’ll call that list as Hierarchy. (Note that it’s in class FConfigBranch, and technically, the type of Hierarchy is class FConfigFileHierarchy, which inherits from TMap<int32, FString>).

Hierarchy of ini file - in the case of the plugin

In the case of the plugin, the rules are the same, but the content is different.

When reading an ini file belonging to a plugin, the list of Hierarchy that it attempts to load is like below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[0] = L"../../../Engine/Config/PluginBase.ini"
[1] = L"E:/GitHubDesktop/MyProjectName/Plugins/ConfigTestEnginePlugin/Config/BaseMyIniNameInPlugin.ini"
[2] = L"E:/GitHubDesktop/MyProjectName/Plugins/ConfigTestEnginePlugin/Config/DefaultMyIniNameInPlugin.ini"
[3] = L"E:/GitHubDesktop/MyProjectName/Plugins/ConfigTestEnginePlugin/Config/VulkanPC/VulkanPCMyIniNameInPlugin.ini"
[4] = L"E:/GitHubDesktop/MyProjectName/Plugins/ConfigTestEnginePlugin/Config/Windows/WindowsMyIniNameInPlugin.ini"
[5] = L"E:/GitHubDesktop/MyProjectName/Config/DefaultMyIniNameInPlugin.ini"
[6] = L"E:/GitHubDesktop/MyProjectName/Config/VulkanPC/VulkanPCMyIniNameInPlugin.ini"
[7] = L"E:/GitHubDesktop/MyProjectName/Config/Windows/WindowsMyIniNameInPlugin.ini"
[8] = L"E:/GitHubDesktop/MyProjectName/Platforms/VulkanPC/Config/VulkanPCMyIniNameInPlugin.ini"
[9] = L"E:/GitHubDesktop/MyProjectName/Platforms/Windows/Config/WindowsMyIniNameInPlugin.ini"

It’s much shorter than a module!!!

If you want to see the source code of where this part is created, see this footnote 2

Hierarchy and final ini files are separate

But… Something seems to be missing, yes. In the above Hierarchy, there is no Final ini file, which is most important to us.

In the example above, Final ini file for MyIniNameInModule must be in folder /Saved/Config/WindowsEditor/, which is under the folder of the project folder, E:/GitHubDesktop/MyProjectName/. In the case of the module, it should be MyIniNameInModule.ini. In the case of the module, it should be MyIniNameInModule.ini. In the case of the plugin, it should be MyIniNameInPlugin.ini.

And our editor or PIE game saves config things into that ini file.

As per the example above, the path to the ini for the module and the ini for the plugin would be one of the following (the locations were explained before.

1
2
3
4
5
6
7
8
9
//Module
E:/GitHubDesktop/MyProjectName/Saved/Config/WindowsEditor/MyIniNameInModule.ini
C:/MyPackageFolder/Saved/Config/Windows/MyIniNameInModule.ini
C:/Users/MyUserName/AppData/Local/MyProjectName/Saved/Config/Windows/MyIniNameInModule.ini

//Plugin
E:/GitHubDesktop/MyProjectName/Saved/Config/WindowsEditor/MyIniNameInPlugin.ini
C:/MyPackageFolder/Saved/Config/Windows/MyIniNameInPlugin.ini
C:/Users/MyUserName/AppData/Local/MyProjectName/Saved/Config/Windows/MyIniNameInPlugin.ini

At now, let’s remember that Hierarchy and Final ini file are separate.

This part is related to the cause of failure of the load of the ini file, which we will mention later.

Why ini file not saved

The reason is that the result value of the bCanSaveAllSections item in the [SectionsToSave] section becomes false, when all the ini files of Hierarchy and the final ini file are combined.

ini save behavior

First, the flow rules of the code are as follows. The engine has a class called FConfigFile that contains the contents of the ini file. And,

  • If the bCanSaveAllSections value of FConfigFile is true,
    • Any class can be recorded in the ini file.
  • If the bCanSaveAllSections value of FConfigFile is false,
    • Only the classes explicitly specified in [SectionsToSave] can be saved in the ini file.
      • If the class is not explicitly specified in [SectionsToSave] when saving, the contents for the class are deleted.

The default value of bCanSaveAllSections when creating FConfigFile is true. And right after creation, the engine reads the value of bCanSaveAllSections from the ini file and writes it here. (Actually, there is some additional processing, which will be covered later in this post.

Now, let’s look at it in more detail, per module and plugin.

Why ini file not saved, in the module

We can see this if we open the engine’s /Engine/Config/Base.ini. It just says the following there. (If you actually open it, there are comments and other content, but I omitted them here.)

1
2
[SectionsToSave]
bCanSaveAllSections=false

All modules’ ini files inherit this. Because, the first entry in the module’s Hierarchy is always /Engine/Config/Base.ini.

Therefore, if you want to save something in the module’s ini file,

  • At the later order of Hierarchy,
    • We must overwrite the value of bCanSaveAllSections in the [SectionsToSave] section with true, or
    • We must add the name of the class explicitly.
      • How to explicitly add a class name has been described previously.

Just in case, I added bCanSaveAllSections=true to the Final ini file and tested it. I saw that it worked properly. In this case, even if the value of bCanSaveAllSections inherited from the previous files in the Hierarchy was false, it was saved.3

Why ini file succeed to save, even if bCanSaveAllSections=true is not added

Unlike modules, all plugin ini files inherit from the engine’s /Engine/Config/PluginBase.ini. As you might expect, its contents are as follows. (Of course, if you open this file, you’ll find comments and other content, but I’ve omitted them here.)

1
2
[SectionsToSave]
bCanSaveAllSections=true

So, contrary to modules, the default value of bCanSaveAllSections in ini of the plugin is true. If we want to restrict the classes that will be saved in the ini file in our plugin, we need to overwrite this value to false.

Why ini file not loaded

ini load rule

  1. Before reading ini,
  2. Among the paths in Hierarchy,
  3. If there are no files that exist actually, except the first entry in Hierarchy,
  4. It does not load Final ini file.

Note that the first entry in Hierarchy, mentioned in point 3 above, is Engine/Config/Base.ini for modules, and Engine/Config/PluginBase.ini for plugins. What this file is is important for saving, but not for loading. In the contrary, it is important to remember that the existence of the file is always guaranteed, and it is excluded from counting the number of files loaded at the same time. In other words, except for this first entry, there must be at least one entry somewhere, otherwise final ini file will not load.

For the call stack for the ini load failure, see the following footnote:4

ini load failure in the module

As a result, if there is one file that needs to be added statically for a project in the module’s Hierarchy, it is the Default ini file, which will be placed under the Config folder in the project folder. So if we didn’t add this file, in most cases the Final ini file would not be able to be loaded.

In theory, adding a Base ini file for BaseIniName to the engine folder might make the final ini file load. However, unless you have very specific requirements for versioning conditions, it’s probably better for these files to exist in the project folder rather than the engine folder.

ini load failure in the plugin

The manner in which plugins read ini is slightly different from modules.

When using an ini file in a plugin, if the name of the ini file is in a “known” category, it behaves the same as a module.

On the other hand, in the plugin, if you are not using additional c++ code, and if you are not using the “known” category, the following restriction is coming.

  • In the plugin, only the ini file with the same BaseIniName as the name of the plugin is available.
  • Also, to load this ini file, the Default ini file for this must be added to the Config folder in the plugin folder.

For example, if the name of the plugin is MyPlugin,

  • If you add DefaultMyPlugin.ini to the Config folder in the plugin folder, as the Default ini file
  • Then, the Final ini file MyPlugin.ini will be loaded.

This is because, when the engine starts, it only loads the Default ini file based on the plugin’s name. See the following comments for the relevant source code and callstack: 5

More detail: GConfig, FConfigCacheIni, FConfigBranch

The specific location where the ini file is stored in the engine is the global variable GConfig: GitHub Link The type of this is FConfigCacheIni.

FConfigCacheIni has many items of FConfigBranch inside it, which contains the data corresponding to the BaseIniName. The engine loads the ini file, puts it into this FConfigBranch for it to use, and when it needs to save the ini file, it saves the contents of this FConfigBranch to a file. Please note that only the contents that are different from the default value will be saved. Items with the same value as ClassDefaultObject will not be saved.

Other things I want to record

Example of reading a plugin’s ini file inside engine source

You can see an example of reading a plugin’s ini file from the engine source in the ChaosVD plugin. GitHub Link Because this plugin has bCanSaveAllSections=true at the top of its DefaultChaosVD.ini, you may think that you need to add bCanSaveAllSections=true to your plugin as well. However, for the reason mentioned earlier, you don’t need to add this.

Small Exception to bCanSaveAllSections

This is something I covered in a previous post. The engine sets the final value of bCanSaveAllSections for an ini file with the following code: (I’ve summarised the code a bit, you can see the full code at the GitHub Link )

1
2
3
4
5
bool bIsUserFile = BaseIniName.Contains(TEXT("User"));
bool bIsEditorSettingsFile = BaseIniName.Contains(TEXT("Editor")) && BaseIniName != TEXT("Editor");

//bLocalSaveAllSections is the value read from ini file.
FinalFile.bCanSaveAllSections = bLocalSaveAllSections || bIsUserFile || bIsEditorSettingsFile;

In other words, it will save everything without restricting what is saved in the following situations.

  • bCanSaveAllSections in the ini file is true
  • Or BaseIniName contains the string User
  • Or BaseIniName is not Editor and contains the string Editor.
    • For example, Editor is not ok, but EditorSettings is ok.

Different behavior of EditorLayout

Among the BaseIniNames, EditorLayout behaves a little specially.

Firstly, it has a different location where it is saved. It is saved in the following folder

C:/Users/MyUserName/AppData/Local/UnrealEngine/5.5/Saved/Config/WindowsEditor/EditorLayout.ini

In the engine, there is a structure called FLayoutSaveRestore that is responsible for saving and loading BaseIniName. FLayoutSaveRestore primarily uses the Json format for saving and loading the editor’s layout. It also uses the ini format, but only as a secondary means of backwards compatibility.

In addition, unless you add some separate c++ code, if you save something in EditorLayout, the saved content will be erased the next time the editor is closed.

Assuming the user has saved something in EditorLayout, let’s follow the steps below.

Load 6

  • If there is an Editor layout saved in Json, the engine will use the content saved in Json.
    • In this case, the ini file is not read, which means that the content of the ini for the EditorLayout inside GConfig will is empty. We assume that we have passed this case.
  • If no Editor Layout is stored as Json, the Editor Layout is read from the ini file and used.

Save 7

  • Save as both Json and ini.
    • If the engine used the data loading from the Json format, the ini content for the EditorLayout inside GConfig is empty. Engine adds the contents of our new Editor Layout to this empty content, and then overwrites it in the ini file for EditorLayout.

This is a typical data loss that occurs when saving without loading.

Therefore, do not use EditorLayout for anything other than its intended purpose. I recommend that you use the EditorLayout only through FLayoutSaveRestore.

After ini fails to load, ini file is deleted in package builds only, if ini contents are not modified

This difference can be observed by looking at the behaviour of the Config for UCustomButBaseNameHasUser in the sample project.

The original purpose was to test the case where the name of the ini file contains User and at the same time the Default ini file does not exist. However, I noticed that the behaviour is different when in the editor and when in a package build. While this is unlikely to happen in the intended usage scenario, I wanted to make a note of it for reference in case I encounter a similar case.

The conditions that cause the difference, and the difference itself, are as follows.

  • Set a string containing User in the BaseIniName, and not setting a Default ini file.
  • After running the editor or the packaged build, change the value of the object and save the contents to an ini file.
  • Close the editor or the packaged build.
  • Run again the editor or the packaged build. At this point, the ini file is not loaded because there is no Default ini file.
  • Close the Editor or the packaged build without doing anything. At this point,
    • The ini file of the editor will remain as it was previously saved.
    • The ini file of the package build is deleted.

Cause: Value of the variable Dirty in FConfigFile InMemoryFile

This difference depends on the value of Dirty in FConfigFile InMemoryFile inside FConfigBranch, which exists inside GConfig.

  • The value of Dirty in the FConfigFile is false at construction time.
  • In the editor, the value of Dirty remains false if there are no changes.
  • However, in package builds, the value of Dirty is set to true right after the load.

The location ini file is deleted

This difference affects the saving process of ini files as follows.

  • If the value of Dirty in InMemoryFile is false, the function exits at the beginning of the function.
  • If the Dirty value of InMemoryFile is true, the engine processes what is saved in the ini file. If its length is zero, the engine deletes the file.

The difference in this saving process can be seen in the code of the static bool SaveBranch(FConfigBranch& Branch) in Source/Runtime/Core/Private/Misc/ConfigCacheIni.cpp. GitHub Link

At the beginning of the code, at line 866, if Branch.InMemoryFile.Dirty is true, it exits.

Later in the code, if the length of the output to save is zero and bBuiltString is false, there is a part that removes the file.

The call stack at this point looked like this.

1
2
3
4
5
6
SaveBranch(FConfigBranch &) ConfigCacheIni.cpp:896
FConfigCacheIni::Flush(bool, const FString &) ConfigCacheIni.cpp:3879
FConfigCacheIni::Exit() ConfigCacheIni.cpp:4482
[Inlined] FEngineLoop::AppExit() LaunchEngineLoop.cpp:6933
[Inlined] LaunchWindowsShutdown() LaunchWindows.cpp:306
WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:318

The location of the Dirty value of FConfigFile InMemoryFile changes right after loading

The reason why the value of Dirty of InMemoryFile becomes true only in package builds after loading the ini is as follows.

  1. First, the value of the global variable GDefaultReplayMethod is different between the editor and the package build: GitHub Link
  2. Based on the value of GDefaultReplayMethod, the value of ReplayMethod, which is FConfigBranch’s member variable, is set differently in FConfigBranch constructor. GitHub Link
  3. The value of ReplayMethod, which is the FConfigBranch’s member variable, determines how the ini file is read from the FConfigContext::LoadIniFileHierarchy function and written to the InMemoryFile. This is where the difference occurs. GitHub Link

Let’s split the chapters once, and then continue with the explanation.

What Happens in FConfigContext::LoadIniFileHierarchy

To summarise, this is because FConfigFile::ApplyFile doesn’t copy the value of Dirty.

What happens in FConfigContext::LoadIniFileHierarchy, unless Branch->ReplayMethod is EBranchReplayMethod::NoReplay, is roughly like this.

  • It reads the contents of the ini file, and writes it to Branch->CombinedStaticLayers.
  • It copys the contents of Branch->CombinedStaticLayers to Branch->InMemoryFile.

The function used to read the ini file here is different depending on the value of Branch->ReplayMethod, which is determined by whether it is the editor or the package build.

Either way, it will call FillFileFromBuffer in the ConfigCacheIni.cpp file.

And in this FillFileFromBuffer function, the value of Dirty is set to true.8

For package builds, i.e., when Branch->ReplayMethod is EBranchReplayMethod::DynamicLayerReplay, the content read from the ini file is written directly to itself via FConfigFile::FillFileFromDisk, a member function of CombinedStaticLayers.

But in the editor, that is, if Branch->ReplayMethod is EBranchReplayMethod::FullReplay,

  • It adds a new FConfigCommandStream item to Branch->StaticLayers.
  • Using the FConfigCommandStream::FillFileFromDisk function, it writes the contents read to the added FConfigCommandStream item,
  • And it uses FConfigFile::ApplyFile function to copy from the FConfigCommandStream item to Branch->CombinedStaticLayers.
    • But, the FConfigFile::ApplyFile function does not copy the value of the Dirty variable in the FConfigCommandStream to the Dirty in the FConfigFile. Because of this, the Dirty in Branch->CombinedStaticLayers will remain false if the engine goes through this route.

Either way, the FConfigContext::LoadIniFileHierarchy function ends with the contents of Branch->CombinedStaticLayers copied to Branch->InMemoryFile.

And as mentioned at the beginning, whether or not the engine can arrive to the part where the engine deletes the file depends on what the Dirty value of FConfigBranch’s InMemoryFile is.

Note

I started this post because I thought that my future self would definitely get confused or make mistakes in this area again. To be honest, this is not the first time I have spent a long time debugging Config ini files since I started using Unreal Engine. It has happened many times. I will do in future some times. That is why I am writing this.

If you find anything wrong with this post, please let me know!


  1. If you want to see how this Hierarchy is created, see below.

    • Function void FConfigContext::AddStaticLayersToHierarchy GitHub Link
    • Global variable array FConfigLayer GConfigLayers GitHub Link

    Here’s an example of the call stack when Hierarchy is created:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    FConfigContext::AddStaticLayersToHierarchy(TArray<> *, bool) ConfigContext.cpp:747
    FConfigContext::PerformLoad() ConfigContext.cpp:535
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    UClass::GetConfigName() Class.cpp:6740
    GetConfigFilename(UObject *) Obj.cpp:2167
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2918
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2783
    FObjectInitializer::PostConstructInit() UObjectGlobals.cpp:4011
    FObjectInitializer::~FObjectInitializer() UObjectGlobals.cpp:3879
    UClass::CreateDefaultObject() Class.cpp:4882
    UClass::InternalCreateDefaultObjectWrapper() Class.cpp:5492
    [Inlined] UClass::GetDefaultObject(bool) Class.h:3409
    UObjectLoadAllCompiledInDefaultProperties(TArray<> &) UObjectBase.cpp:811
    ProcessNewlyLoadedUObjects(FName, bool) UObjectBase.cpp:906
    [Inlined] Invoke(void (*const &)(FName, bool), FName &&, bool &&) Invoke.h:47
    [Inlined] UE::Core::Private::Tuple::TTupleBase::ApplyAfter(void (*const &)(FName, bool), FName &&, bool &&) Tuple.h:317
    TBaseStaticDelegateInstance::ExecuteIfSafe(FName, bool) DelegateInstancesImpl.h:777
    [Inlined] TMulticastDelegateBase::Broadcast(FName, bool) MulticastDelegateBase.h:257
    TMulticastDelegate::Broadcast(FName, bool) DelegateSignatureImpl.inl:1079
    FModuleManager::LoadModuleWithFailureReason(FName, EModuleLoadResult &, ELoadModuleFlags) ModuleManager.cpp:821
    FModuleDescriptor::LoadModulesForPhase(Type, const TArray<> &, TMap<> &) ModuleDescriptor.cpp:753
    FProjectManager::LoadModulesForProject(Type) ProjectManager.cpp:60
    FEngineLoop::LoadStartupModules() LaunchEngineLoop.cpp:4702
    FEngineLoop::PreInitPostStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:3959
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4444
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  2. In the caste of the plugin, creation of the Hierarchy is done by the same function void FConfigContext::AddStaticLayersToHierarchy GitHub Link as in the case of the module above. However, since the calling time and conditions are different, it is based on FConfigLayer GPluginLayers GitHub Link instead of FConfigLayer GConfigLayers.

    This part is done through a separate process, much faster than the loading of the previous module plugin. The call stack is as follows:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    FConfigContext::AddStaticLayersToHierarchy(TArray<> *, bool) ConfigContext.cpp:764
    FConfigContext::PerformLoad() ConfigContext.cpp:535
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    FConfigContext::Load(const wchar_t *) ConfigContext.cpp:296
    FPluginManager::ConfigureEnabledPlugins() PluginManager.cpp:1983
    FPluginManager::LoadModulesForEnabledPlugins(Type) PluginManager.cpp:2836
    FEngineLoop::AppInit() LaunchEngineLoop.cpp:6489
    FEngineLoop::PreInitPreStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:2924
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4437
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  3. I felt like I had committed a great sin, so I removed it as soon as I tested it.

    Why this is a huge sin is because this is a file that is not usually put into version control and is often changed by editors or packaged programs. If you put bCanSaveAllSections=true here and think your ini file is saving properly, and then later on, either in a packaged program, or in someone else’s place, or if you delete all your existing ini files and get new work from a new version control program, you will suddenly find that your ini file, which was saving fine up to that point, won’t save. It’ll be a pain to find out why, and when you do, you’ll probably blame yourself. ↩︎

  4. The call stack when ini load fails is as below. Because LoadIniFileHierarchy returns false, it can not reach the part where the Final ini file is loaded in GenerateDestIniFile. GitHub Link This is an example for modules, but it shouldn’t be much different for plugins: the highlighted part of the call stack below would be the same.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    FConfigContext::LoadIniFileHierarchy() ConfigContext.cpp:935
    FConfigContext::GenerateDestIniFile() ConfigContext.cpp:1033
    FConfigContext::PerformLoad() ConfigContext.cpp:540
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    UClass::GetConfigName() Class.cpp:6740
    GetConfigFilename(UObject *) Obj.cpp:2167
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2918
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2783
    FObjectInitializer::PostConstructInit() UObjectGlobals.cpp:4011
    FObjectInitializer::~FObjectInitializer() UObjectGlobals.cpp:3879 //FObjectInitializer 의 소멸자라 발견하기가 까다롭습니다.
    UClass::CreateDefaultObject() Class.cpp:4882
    UClass::InternalCreateDefaultObjectWrapper() Class.cpp:5492
    [Inlined] UClass::GetDefaultObject(bool) Class.h:3409
    UObjectLoadAllCompiledInDefaultProperties(TArray<> &) UObjectBase.cpp:811
    ProcessNewlyLoadedUObjects(FName, bool) UObjectBase.cpp:906
    [Inlined] Invoke(void (*const &)(FName, bool), FName &&, bool &&) Invoke.h:47
    [Inlined] UE::Core::Private::Tuple::TTupleBase::ApplyAfter(void (*const &)(FName, bool), FName &&, bool &&) Tuple.h:317
    TBaseStaticDelegateInstance::ExecuteIfSafe(FName, bool) DelegateInstancesImpl.h:777
    [Inlined] TMulticastDelegateBase::Broadcast(FName, bool) MulticastDelegateBase.h:257
    TMulticastDelegate::Broadcast(FName, bool) DelegateSignatureImpl.inl:1079
    FModuleManager::LoadModuleWithFailureReason(FName, EModuleLoadResult &, ELoadModuleFlags) ModuleManager.cpp:821
    FModuleDescriptor::LoadModulesForPhase(Type, const TArray<> &, TMap<> &) ModuleDescriptor.cpp:753
    FProjectManager::LoadModulesForProject(Type) ProjectManager.cpp:60
    FEngineLoop::LoadStartupModules() LaunchEngineLoop.cpp:4702
    FEngineLoop::PreInitPostStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:3959
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4444
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  5. Here is the code that loads the ini for the plugin: GitHub Link You can see that it only loads the ini file for the name of the plugin.

    The call stack at this point is as follows:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    FPluginManager::ConfigureEnabledPlugins() PluginManager.cpp:1969
    FPluginManager::LoadModulesForEnabledPlugins(Type) PluginManager.cpp:2836
    FEngineLoop::AppInit() LaunchEngineLoop.cpp:6489
    FEngineLoop::PreInitPreStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:2924
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4437
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  6. You can see the code for saving EditorLayout here: GitHub Link

    It saves the contents to GConfig and saves it as a separate file as Json.

    The call stack at the time of saving is as follows.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    FLayoutSaveRestore::SaveToConfig(const FString &, const TSharedRef<> &) LayoutService.cpp:220
    [Inlined] Invoke(void (FMainFrameHandler::*)(const TSharedRef<> &), FMainFrameHandler *&, const TSharedRef<> &) Invoke.h:66
    [Inlined] UE::Core::Private::Tuple::TTupleBase::ApplyAfter(void (FMainFrameHandler::*&)(const TSharedRef<> &), FMainFrameHandler *&, const TSharedRef<> &) Tuple.h:317
    V::TBaseRawMethodDelegateInstance::ExecuteIfSafe(const TSharedRef<> &) DelegateInstancesImpl.h:533
    [Inlined] TDelegate::ExecuteIfBound(const TSharedRef<> &) DelegateSignatureImpl.inl:634
    FTabManager::SavePersistentLayout() TabManager.cpp:967
    FGlobalTabmanager::SaveAllVisualState() TabManager.cpp:2767
    FMainFrameHandler::ShutDownEditor() MainFrameHandler.cpp:142
    ...(Omitted.)
     ↩︎
  7. You can see the code for loading EditorLayout in this link.: GitHub Link If loading with json succeeds, it does not read the ini file.

    Below is the call stack when loading.

    1
    2
    3
    4
    5
    6
    7
    
    FLayoutSaveRestore::LoadFromConfigPrivate(const FString &, const TSharedRef<> &, EOutputCanBeNullptr, const bool, TArray<> &) LayoutService.cpp:250
    FLayoutSaveRestore::LoadFromConfig(const FString &, const TSharedRef<> &, EOutputCanBeNullptr, TArray<> &) LayoutService.cpp:238
    FMainFrameModule::CreateDefaultMainFrameAuxiliary(const bool, const bool, const bool) MainFrameModule.cpp:297
    EditorInit(IEngineLoop &) UnrealEdGlobals.cpp:178
    GuardedMain(const wchar_t *) Launch.cpp:165
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  8. This is where the Dirty value changes to true when loading the ini file.

    Code: GitHub Link

    Whether it is run in the editor or in the package build, the point where the Dirty value changes to true is the same.

    The call stack looks slightly different when in the editor and the packaged build, but the overall shape is similar.

    The call stack in the package build is as follows. Note that line number 988 in the call stack FConfigContext::LoadIniFileHierarchy() is incorrect. The correct location is line 973. This is probably due to compiler optimization.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    FillFileFromBuffer<>(FConfigFile *, TStringView<>, bool, const FString &) ConfigCacheIni.cpp:1673
    FillFileFromDisk<>(FConfigFile *, const FString &, bool) ConfigCacheIni.cpp:1692
    FConfigContext::LoadIniFileHierarchy() ConfigContext.cpp:988
    FConfigContext::GenerateDestIniFile() ConfigContext.cpp:1033
    FConfigContext::PerformLoad() ConfigContext.cpp:540
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    UClass::GetConfigName() Class.cpp:6740
    [Inlined] GetConfigFilename(UObject *) Obj.cpp:2167
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2918
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2783
    FObjectInitializer::PostConstructInit() UObjectGlobals.cpp:4011
    FObjectInitializer::~FObjectInitializer() UObjectGlobals.cpp:3879
    UClass::CreateDefaultObject() Class.cpp:4882
    [Inlined] UClass::GetDefaultObject(bool) Class.h:3409
    UObjectLoadAllCompiledInDefaultProperties(TArray<> &) UObjectBase.cpp:811
    ProcessNewlyLoadedUObjects(FName, bool) UObjectBase.cpp:906
    FEngineLoop::PreInitPostStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:3784
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4444
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    GuardedMainWrapper(const wchar_t *) LaunchWindows.cpp:123
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:277
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317

    Below is the call stack in the editor.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    FillFileFromBuffer<>(FConfigCommandStream *, TStringView<>, bool, const FString &) ConfigCacheIni.cpp:1673
    FillFileFromDisk<>(FConfigCommandStream *, const FString &, bool) ConfigCacheIni.cpp:1692
    FConfigContext::LoadIniFileHierarchy() ConfigContext.cpp:965
    FConfigContext::GenerateDestIniFile() ConfigContext.cpp:1033
    FConfigContext::PerformLoad() ConfigContext.cpp:540
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    UClass::GetConfigName() Class.cpp:6740
    GetConfigFilename(UObject *) Obj.cpp:2167
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2918
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2783
    FObjectInitializer::PostConstructInit() UObjectGlobals.cpp:4011
    FObjectInitializer::~FObjectInitializer() UObjectGlobals.cpp:3879
    UClass::CreateDefaultObject() Class.cpp:4882
    UClass::InternalCreateDefaultObjectWrapper() Class.cpp:5492
    [Inlined] UClass::GetDefaultObject(bool) Class.h:3409
    UObjectLoadAllCompiledInDefaultProperties(TArray<> &) UObjectBase.cpp:811
    ProcessNewlyLoadedUObjects(FName, bool) UObjectBase.cpp:906
    [Inlined] Invoke(void (*const &)(FName, bool), FName &&, bool &&) Invoke.h:47
    [Inlined] UE::Core::Private::Tuple::TTupleBase::ApplyAfter(void (*const &)(FName, bool), FName &&, bool &&) Tuple.h:317
    TBaseStaticDelegateInstance::ExecuteIfSafe(FName, bool) DelegateInstancesImpl.h:777
    [Inlined] TMulticastDelegateBase::Broadcast(FName, bool) MulticastDelegateBase.h:257
    TMulticastDelegate::Broadcast(FName, bool) DelegateSignatureImpl.inl:1079
    FModuleManager::LoadModuleWithFailureReason(FName, EModuleLoadResult &, ELoadModuleFlags) ModuleManager.cpp:821
    FModuleDescriptor::LoadModulesForPhase(Type, const TArray<> &, TMap<> &) ModuleDescriptor.cpp:753
    FProjectManager::LoadModulesForProject(Type) ProjectManager.cpp:60
    FEngineLoop::LoadStartupModules() LaunchEngineLoop.cpp:4702
    FEngineLoop::PreInitPostStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:3959
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4444
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
tags