kockleong
(kockleong)
November 10, 2022, 8:22am
1
Hi all esteemed experts in this amazing community,
I am trying to create mods controlled by text file (I chose .ini as normal json does not support comment).
At onStart of the gameInstance, the program will read the ini file from specified location using FConfigFile.Read(). Modders can modify via UE5 UI. And then will save the configuration upon quitting.
However, after writing to the config file, all previous comments (for guiding modders who want to change directly in the config file) will disappear.
Is there anyway to save the comments?
If no, is there anyway to make FConfigFile inherit from unordered map (I believe now is from TMAP which is from ordered map)? So that I can use #=XXX for the comments? I need the ini file to be in exact order so that the comment can appear for the correct variable
Thanks in advance
Really appreciate any assistance
Chatouille
(Chatouille)
November 10, 2022, 1:41pm
2
homemade experiment
bool IniSaveWithComments(const FString& SourceFile, FConfigFile& Config, const FString& DestFile);
bool ShouldExportQuotedString(const FString& PropertyValue);
void ASomeActor::IniTest()
{
FString InPath = "D:/Test.ini";
FString OutPath = "D:/TestOut.ini";
FConfigFile ConfigData;
ConfigData.Read(InPath);
IniSaveWithComments(InPath, ConfigData, OutPath);
}
struct FSectionComments
{
FString SectionName;
FString SectionComment;
TMap<FString, FString> KeyComments;
};
bool IniSaveWithComments(const FString& SourceFile, FConfigFile& Config, const FString& DestFile)
{
// Step 1 : read original as text
// -- Code adapted from FConfigFile::Read
FString Text;
FString FinalSourceFile = SourceFile;
const bool bOverride = FConfigFile::OverrideFileFromCommandline(FinalSourceFile);
if (bOverride)
FFileHelper::LoadFileToString(Text, &IPlatformFile::GetPlatformPhysical(), *FinalSourceFile);
else
FFileHelper::LoadFileToString(Text, *FinalSourceFile);
// Step 2 : Generate a comments map for Sections & Keys
TMap<FString, FSectionComments> CommentsMap;
// -- Code adapted from FConfigFile::ProcessInputFileContents
const TCHAR* Ptr = Text.Len() > 0 ? *Text : nullptr;
FSectionComments* CurrentSection = nullptr;
FString CurrentComment;
while (Ptr != nullptr && *Ptr != 0)
{
// Advance past new line characters
while (*Ptr == '\r' || *Ptr == '\n')
{
Ptr++;
}
// read the next line
FString Line;
int32 LinesConsumed = 0;
FParse::LineExtended(&Ptr, Line, LinesConsumed, false);
Line.TrimStartAndEndInline();
// Consider lines that start with ; as comments
if (Line.StartsWith(";"))
{
CurrentComment += Line + LINE_TERMINATOR;
continue;
}
// If the first character in the line is [ and last char is ], this line indicates a section name
if (Line.StartsWith("[") && Line.EndsWith("]"))
{
FString SectionName = Line.Mid(1, Line.Len() - 2);
CurrentSection = &CommentsMap.FindOrAdd(SectionName);
CurrentSection->SectionName = SectionName;
CurrentSection->SectionComment = CurrentComment;
CurrentComment.Empty();
}
// Otherwise, if we're currently inside a section, and we haven't reached the end of the stream
else if (CurrentSection)
{
int32 i = Line.Find("=");
if (i > 0)
{
FString Key = Line.Left(i);
Key.TrimStartAndEndInline();
FString& KeyComment = CurrentSection->KeyComments.FindOrAdd(Key);
KeyComment += CurrentComment;
CurrentComment.Empty();
}
}
}
// Step 3 = Generate ini text with comments
FString FinalText;
for (const auto& Pair : Config)
{
const FString& SectionName = Pair.Key;
const FConfigSection& Section = Pair.Value;
FinalText.Append(LINE_TERMINATOR); // space between sections
FSectionComments* Comments = CommentsMap.Find(SectionName);
if (Comments && !Comments->SectionComment.IsEmpty())
{
FinalText.Append(Comments->SectionComment);
}
FinalText.Append("[" + SectionName + "]" + LINE_TERMINATOR);
TSet<FString> AlreadyWrote;;
for (FConfigSection::TConstIterator It2(Section); It2; ++It2)
{
const FString& PropertyName = It2.Key().ToString();
if (AlreadyWrote.Contains(PropertyName))
continue;
AlreadyWrote.Add(PropertyName);
if (Comments && Comments->KeyComments.Contains(PropertyName))
{
FString Comm = Comments->KeyComments.FindAndRemoveChecked(PropertyName);
if (!Comm.IsEmpty())
{
FinalText.Append(Comm);
}
}
TArray<const FConfigValue*> CompletePropertyToWrite;
Section.MultiFindPointer(FName(*PropertyName), CompletePropertyToWrite, true);
for (const FConfigValue* ConfigValue : CompletePropertyToWrite)
{
// -- FConfigFile::AppendExportedPropertyLine is not exported :(
const FString& Value = ConfigValue->GetSavedValue();
FinalText.Append(PropertyName + "=");
if (ShouldExportQuotedString(Value))
{
FinalText.Append("\"" + Value.ReplaceCharWithEscapedChar() + "\"");
}
else
{
FinalText.Append(Value);
}
FinalText.Append(LINE_TERMINATOR);
}
}
}
// Step 4 = Save file
// -- Code from SaveConfigFileWrapper (private)
int32 SavedCount = 0;
FCoreDelegates::PreSaveConfigFileDelegate.Broadcast(*DestFile, FinalText, SavedCount);
bool bLocalWriteSucceeded = FFileHelper::SaveStringToFile(FinalText, *DestFile, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
return SavedCount > 0 || bLocalWriteSucceeded;
}
// -- Copy of FConfigFile::ShouldExportQuotedString
bool ShouldExportQuotedString(const FString& PropertyValue)
{
bool bEscapeNextChar = false;
bool bIsWithinQuotes = false;
// The value should be exported as quoted string if...
const TCHAR* const DataPtr = *PropertyValue;
for (const TCHAR* CharPtr = DataPtr; *CharPtr; ++CharPtr)
{
const TCHAR ThisChar = *CharPtr;
const TCHAR NextChar = *(CharPtr + 1);
const bool bIsFirstChar = CharPtr == DataPtr;
const bool bIsLastChar = NextChar == 0;
if (ThisChar == TEXT('"') && !bEscapeNextChar)
{
bIsWithinQuotes = !bIsWithinQuotes;
}
bEscapeNextChar = ThisChar == TEXT('\\') && bIsWithinQuotes && !bEscapeNextChar;
// ... it begins or ends with a space (which is stripped on import)
if (ThisChar == TEXT(' ') && (bIsFirstChar || bIsLastChar))
{
return true;
}
// ... it begins with a '"' (which would be treated as a quoted string)
if (ThisChar == TEXT('"') && bIsFirstChar)
{
return true;
}
// ... it ends with a '\' (which would be treated as a line extension)
if (ThisChar == TEXT('\\') && bIsLastChar)
{
return true;
}
// ... it contains unquoted '{' or '}' (which are stripped on import)
if ((ThisChar == TEXT('{') || ThisChar == TEXT('}')) && !bIsWithinQuotes)
{
return true;
}
// ... it contains unquoted '//' (interpreted as a comment when importing)
if ((ThisChar == TEXT('/') && NextChar == TEXT('/')) && !bIsWithinQuotes)
{
return true;
}
// ... it contains an unescaped new-line
if (!bEscapeNextChar && (NextChar == TEXT('\r') || NextChar == TEXT('\n')))
{
return true;
}
}
return false;
}
kockleong
(kockleong)
November 11, 2022, 2:10pm
3
Hi ,
Thanks for your code. It works well but I had made some modification to suit my needs.
Really appreciate all your help… Thanks once again…
For anyone who have the same needs as me, below are my scripts (Modified heavily from , although there is some major changes). Please ignore my class name as I’m reusing some classes that I use for JSON testing.
Below is my class header file
A_JsonObj.h (1.5 KB)
Below is my class source file
A_JsonObj.cpp (8.4 KB)
Below is my testing ini file
Intro.ini (1.1 KB)
Below is the saved ini file
introOut.ini (1.1 KB)