Config files: Creating new and saving existing

Look at repro steps

[Attachment Removed]

Steps to Reproduce
One of our in-house tools requires saving and loading of different profiles.

I’ve been using the config system for this with and it has worked for years.

Now it doesn’t.

I am saving my config files to ../Game/Saved/MyConfigFolder/

  /**
   * Finds, loads, or creates the in-memory config file for a config cache filename.
   * 
   * [Content removed] or the return value of GetConfigFilename
   * 
   * @return A new or existing config file
   */
  CORE_API FConfigFile* Find(const FString& InFilename);


When the function above is called with a new file name that does not exist on disk it returns nullptr, instead of a new file as it used to and as it says in the comment it should do.

Flushing my config to a file has also stopped working.

What would be the correct way to go about this?

[Attachment Removed]

Hi [mention removed]​,

After inspecting the function, what it actually does is create the FConfigFile in memory, but not the physical .ini file on disk. So the config file object is generated, but no corresponding file is created on disk. As you mentioned, newer engine versions no longer behave the same way. If you want to generate new config files now, you need to explicitly create the file on disk or register it manually before using the Find() function.

In older versions (such as 5.3), these additional checks were not present inside Find(). Back then, if a file didn’t exist on disk, Unreal would still create an in-memory FConfigFile. Then, when you flushed or wrote data, the .ini file was created automatically. That is no longer the case in newer versions — the behavior is blocked by the call to DoesConfigFileExistWrapper().

In summary: if the file does not already exist in current versions, Find() will not create it. Once the .ini file exists, you can use it as before.

To fix your issue, the best approach would be to ensure the file exists before calling Find().

void UBPLibrary::CreateIniFile(const FString& IniName)
{
    // Build the full path in Config/
    const FString IniPath = FPaths::ProjectConfigDir() / FString::Printf(TEXT("Default%s.ini"), *IniName);
 
    // Check if file already exists
    if (IFileManager::Get().FileExists(*IniPath))
        return; 
 
    // Create the file (empty)
    FFileHelper::SaveStringToFile(TEXT(""), *IniPath);
}

You could also manually create and manage the FConfigFile yourself (as in older versions), but that’s usually more work. Alternatively, you could modify the Find() implementation inside the engine to restore the old behavior if that’s critical for your workflow. Another option would create your own Find function and do a wrapper over it.

Please let me know if you have any questions about this.

Best,

Joan

[Attachment Removed]

Flushing the file to my new path does not seem to work.

What is the point of the NoSave flag?

I have not had to fiddle with this before?

Setting it to true enter som code that looks like it would save the file, but I end up with an empty outputstring and in the end it deletes my newly created file.

	else
	{
		Branch.InMemoryFile.WriteToString(Output, Branch.IniPath);
		bBuiltString = true;
	}
		
	if (bBuiltString && Output.Len() > 0)
	{
		Output = FString(TEXT(";METADATA=(Diff=true, UseCommands=true)" LINE_TERMINATOR_ANSI)) + Output;
		
	
		return SaveConfigFileWrapper(*Branch.IniPath, Output);
	}
 
	// delete any old crusty saved ini files from before we disabled most sections' from writing
	IFileManager::Get().Delete(*Branch.IniPath);

[Attachment Removed]

I also stumbled upon this issue today, where I could not save to the file due to NoSave=true.

I found on a forum post that they got their settings to save by swapping out

SaveConfig(CPF_Config, *relativePath);to

TryUpdateDefaultConfigFile(*relativePath);

I tried it as well and it works for me. But I don’t know if there are any consequences to changing it or why the previous SaveConfig no longer works.

In our case I was trying to save to ../Game/Config/DefaultCustomClass.ini

[Attachment Removed]

Hi everyone,

Inside the engine source code, they used this way to create .ini files. I find it pretty straight forward, letting it here if it is also prefered for anyone:

FConfigFile& NewFile = GConfig->Add(ConfigFile, FConfigFile());
	
NewFile.SetString(TEXT("MySection"), TEXT("Key"), TEXT("MyValue"));
	
const FString FilenamePath = FConfigCacheIni::NormalizeConfigIniPath(FPaths::ProjectConfigDir() + ConfigFile);
 
NewFile.UpdateSinglePropertyInSection(*FilenamePath, TEXT("Key"), TEXT("MySection"));

I’ll ask Epic directly, as this isn’t the first time a user has brought it up. I’ll check with them on the “correct” way to manage custom config files, since recent engine changes have caused several studios to run into issues with this.

Best,

Joan

[Attachment Removed]

Answering here due to threat of closing.

Still want to know how to do it.

[Attachment Removed]

Answering here again due to threat of closing.

Still want to know how to do it.

[Attachment Removed]

I still want to know the answer.

Writing to keep topic from auto closing.

[Attachment Removed]

Hi Jakob,

Joan is looking up the CL for the change, and once I have that I will reach out to the dev side to find out why we decided to alter the logic here. We should have this in the next few days.

Best

Geoff Stacey

Developer Relations

EPIC Games

[Attachment Removed]

Thank you for the answer.

So how do I achieve the original behavior. Can I call add directly?

[Attachment Removed]

Hi Both,

Jakob - I’ve reached out to the developer to clarify if there was an issue we were solving with this change, or if it was an interface clean up (ie a find function is implicitly doing file creation which isn’t an obvious thing for a find to do).

Can I just clarify quickly that the issue boils down to whereas before the call was returning a file, now it is returning a nullptr?

Best

Geoff

[Attachment Removed]

Hi Jakob, can you describe the use case for me please for when this function is called?

I’m aware it is part of your build pipeline, but why does it rely on autogeneration of a file?

Thanks

Geoff Stacey

Developer Relations

EPIC Games

[Attachment Removed]

Hi Geoff,

It is not part of our build process. My mistake. I will try to describe the use case in more detail.

We have a UI debug tool, built in Slate, that has a lot of different options for how to visualize some data, and what data to visualize. It’s animation and gameplay related.

Depending on what part of the game we are debugging we want to display certain things and hide others. It’s easily 20-40 button presses just to set up a a specific profile, which then goes back to default display settings on editor restart.

To allow quick switching between different ways of visualizing, I’ve used the config file system to save the current state of the debug window (not the data, just state of what is displayed).

Every time a button is clicked that changes the state of the debug window, I save the new setting in the current config file (in memory file I was assuming) using GConfig->Set…

void FSaveConfigDialogUtils::SaveConfigFile(const FString& FileName, FString& DefaultConfigFileFullPath)
{
	FString SaveConfigFileName = FileName;
	FPaths::MakeStandardFilename(SaveConfigFileName);
 
// Create the new config file
	FConfigFile* Config = GConfig->Find(SaveConfigFileName);
// Write to the current to make sure we startup with the current setup we just saved	
	GConfig->Flush(false, DefaultConfigFileFullPath);	
	Config->Read(DefaultConfigFileFullPath);
	Config->Dirty = true;
 
// Write to the config new config file, so we can load late
	GConfig->Flush(false, SaveConfigFileName);
}

If I want to load a setting, I use the code below

bool SPaceDebugWindow::LoadConfig(const FString& Name) const
{
	const TCHAR* EngineConfigDir = nullptr;
	FString SourceConfigDir = IMyModule::GetConfigFolderPath();
	FConfigFile* Config = GConfig->Find(DefaultConfigFileFullPath);
 
	if (!Config)
	{
		return false;
	}
 
	return FConfigCacheIni::LoadExternalIniFile(*Config, *Name, EngineConfigDir, *SourceConfigDir, false);
}

The use of GConfig->Get… and GConfig->Set… is everywhere in code, which is why I was hoping you could tell me how to get it working again using the same interface I’ve already been using.

Best,

Jakob

[Attachment Removed]

Hi Jakob,

Thank you for the insight - this has been fed back to our dev side so they are aware of this as a potential use case.

They have suggested that you add this code snippet to create a default file in the cases where you don’t already have the file created and that should then work.

FConfigContext Context = FConfigContext::ReadSingleIntoGConfig();

Context.Load(TEXT("SomePath/Settings.ini);

Best

Geoff

[Attachment Removed]

static FConfigFile& ActiveFile(FConfigContext* Context)
{
	if (Context->ExistingFile) return *Context->ExistingFile;
	if (Context->Branch) return Context->Branch->InMemoryFile;
	unimplemented();
	static FConfigFile Empty;
	return Empty;
}

Now I just hit this assert.

It’s still not saving to my file when I call GConfig->Flush

Nothing gets saved to my existing file, nor to the one I just attempted to create.

[Attachment Removed]

Ah, okay can you post the full code snippet and I’ll find out what is happening? Could be we would be better served adding a FindOrAdd function, either locally or in the codebase

[Attachment Removed]

The full save and load config snippet are posted further up.

The new save snippet with the context that asserts looks like this.

void FSaveConfigDialogUtils::SaveConfigFile(const FString& FileName, FString& DefaultConfigFileFullPath)
{
	FString SaveConfigFullPath = FileName;
	FPaths::MakeStandardFilename(SaveConfigFullPath);
 
	// Check if file already exists
	if (!IFileManager::Get().FileExists(*FileName))
	{
		// Create the file (empty)
		FFileHelper::SaveStringToFile(TEXT(""), *SaveConfigFullPath);
	}
 
	FConfigContext Context = FConfigContext::ReadSingleIntoGConfig();
	Context.Load(*SaveConfigFullPath);
 
	GConfig->Flush(false, DefaultConfigFileFullPath);
 
//Deleted some code here, since nothing works
 
}

[Attachment Removed]

I’ll just rewrite our code, and maybe make my own system. I’m being told GConfig is not made to have different setting to save and load.

I still don’t understand why GConfig->Flush doesn’t work my default ini file.

GConfig->Find documentation states it will create a config if none exists. It doesn’t, so the documentation should be updated.

[Attachment Removed]

Dev here - I had tested the FConfigContext snippet I gave to Geoff:

// add a single, non-hierarchical, Test.ini to GConfig
FString IniPath = TEXT("/Users/josh.adams/Perforce/UE/Test.ini"); 
FConfigContext Context = FConfigContext::ReadSingleIntoGConfig();
Context.Load(*IniPath);
 
// test the value loaded (if the file did not exist, it will fail to find a value and leave it as -1)
int SavedValue = -1;
GConfig->GetInt(TEXT("Blurp"), TEXT("Hi"), SavedValue, IniPath);
UE_LOG(LogInit, Log, TEXT("SavedValue = %d"), SavedValue);
 
// set the value and save out
GConfig->SetInt(TEXT("Blurp"), TEXT("Hi"), 5, IniPath);
GConfig->Flush(false);

The first time it runs, it logs -1, and the second time it logs 5. The file is created as expected:

;METADATA=(Diff=true, UseCommands=true)
[Blurp]
Hi=5

This should take care of your needs to create new file if it doesn’t exist, load it if it does exist, and save whatever you Set it to.

What filename are you using here? I assume you are doing manual config manipulation, not just trying to save to DefaultEngine.ini (as that is already generally handled for ProjectSetting type things). But your variable is called DefaultConfigFileFullPath and you also have FileName, so … I’m not sure exactly what paths are being used here.

[Attachment Removed]