Hello everyone,
I was perusing the forums and found that some people were trying to get their variables from an .ini file. A lot of wonderful folks recommended different solutions but if you want a custom and reliable .ini parser that you can store specific info into and that also has readable data type (well not all of them but the most important ones) I’ve got you covered! This can be used in runtime and can be read immediately upon change, so that you don’t have to restart the game.
The first thing that we should do is add some #include-s.
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "UObject/ObjectMacros.h"
#include "UObject/EnumProperty.h"
These all are part for the course.
Now what we want to do first is slowly go through and create everything that we need.
First we need an .ini file to be created and initialized.
.h
UFUNCTION(BlueprintCallable)
static void CreateCustomIniFile(
const FString& FolderPath,
const FString FileString,
FString SectionName,
FString KeyName,
FString Value
);
.cpp
void YOURCLASS::CreateCustomIniFile(const FString& FolderPath, const FString FileString, FString SectionName, FString KeyName, FString Value)
{
FString FilePath = FPaths::Combine(FolderPath, FileString);
FString Contents = FString::Printf(TEXT("[%s]\n%s=%s\n"), *SectionName, *KeyName, *Value);
FFileHelper::SaveStringToFile(Contents, *FilePath);
}
Be aware that I’m making these in a blueprint library so that I can easily access it anywhere in the project and also access it in Blueprints.
What we’re doing in the code above is to get a specific filepath where we’ll save our newly created .ini file. Then we want to create the initial section with its key and value pair. What I particularly like from the Unreal engine .ini system is the formatting so I’m using their section name formatting.
This code will also create the folder that you specify, if one does not exist. Be aware that if you run this code again, the previous values will be deleted.
Now we want to be able to add a new line to our created .ini file.
.h
UFUNCTION(BlueprintCallable)
static void AddLineToIniFile(
const FString& FilePath,
const FString& SectionName,
const FString& Key,
const FString& Value,
const EConfigValueType& EnumType
);
You’ll notice that there is a custom Enum here and we’re going to be creating our own Enum and Struct so that we can properly input and output variable types. We want to be able to seamlessly get our code from the .ini file, without additional typecasting, formating or parsing. This Enum is defined in another UE class (I selected an UObject for the sake of simplicity).
You have to define the Enum outside of the UE boilerplate initialization code, as it will return an error about the accessibility as its not in the global scope.
The enum is defined like this:
.h (In another file)
UENUM(BlueprintType)
enum class EConfigValueType : uint8
{
Int32 UMETA(DisplayName = "Int32"),
Char UMETA(DisplayName = "Char"),
String UMETA(DisplayName = "String"),
Bool UMETA(DisplayName = "Bool"),
Float UMETA(DisplayName = "Float"),
None UMETA(DisplayName = "None")
};
Now we have a usable Enum that also has the UMETA keyword so that we can find it as an object later on.
We’re now ready to define the actual code that will implement line addition:
void YOURCLASS::AddLineToIniFile(const FString& FilePath, const FString& SectionName, const FString& Key, const FString& Value,const EConfigValueType& EnumType)
{
FString CurrentContents;
FFileHelper::LoadFileToString(CurrentContents, *FilePath);
TArray<FString> Lines;
CurrentContents.ParseIntoArrayLines(Lines);
int32 SectionLineIndex = -1;
for (int32 i = 0; i < Lines.Num(); ++i)
{
if (Lines[i].StartsWith(FString::Printf(TEXT("[%s]"), *SectionName)))
{
SectionLineIndex = i;
break;
}
}
FString MyEnum = EnumToString(TEXT("EConfigValueType"), static_cast<int32>(EnumType));
FString NewLine = FString::Printf(TEXT("%s:%s = %s"), *MyEnum, *Key, *Value);
if (SectionLineIndex == -1)
{
Lines.Add(FString::Printf(TEXT("\n[%s]"), *SectionName));
Lines.Add(NewLine);
}
else
{
Lines.Insert(NewLine, SectionLineIndex + 1);
}
FString NewContents = FString::Join(Lines, TEXT("\n"));
FFileHelper::SaveStringToFile(NewContents, *FilePath);
}
The first thing here that you’ll notice is that there’s a function called “EnumToString” that doesn’t natively exist in UE. We have to create this function ourselves as well, so that we can use the UMETA macro “Display Name” property. This is how we make that function.
h. (where the main code is)
static FString EnumToString(const TCHAR* Enum, int32 EnumValue);
.cpp
FString YOURCLASS::EnumToString(const TCHAR* Enum, int32 EnumValue)
{
const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, Enum, true);
if (!EnumPtr)
{
return NSLOCTEXT("Invalid", "Invalid", "Invalid").ToString();
}
return EnumPtr->GetNameStringByIndex(EnumValue);
}
We want to search for our Enum object, and the only way to make it visible is with the macro, as it will open it to unreal reflection system.
So we’re inputing the path to our file, the name of the section, key and value pair as well as the EnumType (in this case it acts as a type definition).
What’s great is that if our code finds that there exists already a section with the same line that we chose, it will just add it there, at the end. This means that we can have it all tidy!
At this point the generated file should look something like this:
We would ideally like to be able to get a value from the .ini itself and to use a key to find its value. We needed the Enum to set the possible variable type and we will also use this same Enum as output. Because we will be using a struct and we won’t exactly know which variable in the struct is initialized with the data that we want, we will use this Enum output as a switch case to redirect the flow of code depending on the data type.
The struct is defined and initialized like this
.h (the same one that we used for the Enum)
USTRUCT(BlueprintType)
struct FSConfigValue
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
EConfigValueType ValueType = EConfigValueType::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 IntValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString CharValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString StringValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool BoolValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float FloatValue;
FSConfigValue()
{
ValueType = EConfigValueType::None;
IntValue = 0;
CharValue = "Empty";
StringValue = "Empty";
BoolValue = false;
FloatValue = 0.0f;
}
};
All the values are initialized so there is no actual risk of it returning a nullptr, which is nice if you don’t want your engine crashihg .
This struct will be our output file and the code for it is as follows:
.h (where the original code is )
UFUNCTION(BlueprintCallable)
static FSConfigValue FindInIniByKey(
const FString& FilePath,
const FString& Key
);
.cpp
FSConfigValue YOURCLASS::FindInIniByKey(const FString& FilePath, const FString& Key)
{
FSConfigValue Result;
FString FileContents;
FFileHelper::LoadFileToString(FileContents, *FilePath);
TArray<FString> Lines;
FileContents.ParseIntoArrayLines(Lines);
for (const FString& Line : Lines)
{
TArray <FString> TypeAndRest;
Line.ParseIntoArray(TypeAndRest, TEXT(":"), true);
if (TypeAndRest.Num() == 2)
{
TArray<FString> KeyAndValue;
TypeAndRest[1].ParseIntoArray(KeyAndValue, TEXT("="), true);
if(KeyAndValue.Num() == 2 && KeyAndValue[0].TrimStartAndEnd() == Key)
{
FString TypeString = TypeAndRest[0].TrimStartAndEnd();
FString ValueString = KeyAndValue[1].TrimStartAndEnd();
if (TypeString == "Int32")
{
Result.ValueType = EConfigValueType::Int32;
Result.IntValue = FCString::Atoi(*ValueString);
}
else if (TypeString == "Char")
{
Result.ValueType = EConfigValueType::Char;
Result.CharValue = ValueString;
}
else if (TypeString == "String")
{
Result.ValueType = EConfigValueType::String;
Result.StringValue = ValueString;
}
else if (TypeString == "Bool")
{
Result.ValueType = EConfigValueType::Bool;
Result.BoolValue = ValueString.ToBool();
}
else if (TypeString == "Float")
{
Result.ValueType = EConfigValueType::Float;
Result.FloatValue = FCString::Atof(*ValueString);
}
break;
}
}
}
return Result;
}
I know the CHAR and STRING values are saved the same way, but I wanted to differentiate it if possible. Maybe some of y’all find a good use case for it!
The code here is pretty straightforward. We want to find the type and the name of the variable we’re looking for and set the struct values and the output Enum.
Now you can already do a lot with this, but why stop! Let’s make a way to actually modify a value by its key.
.h
UFUNCTION(BlueprintCallable)
static void UpdateIniFileValue(
const FString& FilePath,
const FString& Key,
const FString& NewValue
);
.cpp
void YOURCLASS::UpdateIniFileValue(const FString& FilePath, const FString& Key, const FString& NewValue)
{
FString FileContents;
FFileHelper::LoadFileToString(FileContents, *FilePath);
TArray<FString> Lines;
FileContents.ParseIntoArrayLines(Lines);
bool bUpdated = false;
for (FString& Line : Lines)
{
TArray<FString> TypeAndRest;
Line.ParseIntoArray(TypeAndRest, TEXT(":"), true);
if (TypeAndRest.Num() == 2)
{
TArray<FString> KeyAndValue;
TypeAndRest[1].ParseIntoArray(KeyAndValue, TEXT("="), true);
if (KeyAndValue.Num() == 2 && KeyAndValue[0].TrimStartAndEnd() == Key)
{
KeyAndValue[1] = NewValue;
Line = FString::Printf(TEXT("%s:%s=%s"), *TypeAndRest[0], *Key, *NewValue);
bUpdated = true;
break;
}
}
}
if (bUpdated)
{
FString NewFileContents = FString::Join(Lines, TEXT("\n"));
FFileHelper::SaveStringToFile(NewFileContents, *FilePath);
}
}
This just goes and separates the lines into types, keys, and values and if the key aligns with the one that you inserted, it will change its value.
This is a way I implemented it.
Also don’t forget to include your Enum and struct class into your .cpp file!
Thank you so much if you read this through and if you enjoyed my coding tips!
Cheers.