forked from HARPLab/DReyeVR
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ConfigFile.h
334 lines (302 loc) · 12.2 KB
/
ConfigFile.h
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#pragma once
#include <fstream> // std::ifstream
#include <istream> // std::istream
#include <sstream> // std::istringstream
#include <string>
#include <unordered_map>
const static FString CarlaUE4Path = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
struct ConfigFile
{
ConfigFile() = default; // empty constructor (no params yet)
ConfigFile(const FString &Path, bool bVerbose = true) : FilePath(Path)
{
/// TODO: add feature to "hot-reload" new params during runtime
bSuccessfulUpdate = ReadFile(bVerbose); // ensures all the variables are updated upon construction
// simple sanity check to ensure exporting and importing the same config file works as intended
// (exporting self and creating a new import should be equal to self)
static bool bSanityCheck = this->IsEqual(ConfigFile::Import(this->Export()));
ensureMsgf(bSanityCheck, TEXT("Sanity check for ConfigFile import/export failed!"));
}
template <typename T> bool Get(const FString &Section, const FString &Variable, T &Value) const
{
const std::string SectionStdStr(TCHAR_TO_UTF8(*Section));
const std::string VariableStdStr(TCHAR_TO_UTF8(*Variable));
return GetValue(SectionStdStr, VariableStdStr, Value);
}
template <typename T> T Get(const FString &Section, const FString &Variable) const
{
T Value;
/// TODO: implement exception when Get returns false?
Get(Section, Variable, Value);
return Value;
}
template <typename T>
T GetConstrained(const FString &Section, const FString &Variable, const std::unordered_set<T> &Options,
const T &DefaultValue) const
{
T Value = Get<T>(Section, Variable);
if (Options.find(Value) == Options.end())
{
// not found within the constrained available options
Value = DefaultValue;
}
return Value;
}
bool bIsValid() const
{
return bSuccessfulUpdate;
}
bool IsEqual(const ConfigFile &Other, bool bPrintWarning = false) const
{
// calculates if A subset B and B subset A
return this->IsSubset(Other, bPrintWarning) && Other.IsSubset(*this, bPrintWarning);
}
bool IsSubset(const ConfigFile &Other, bool bPrintWarning = false) const
{
// only checking that this is a perfect subset of Other
// => Other can contain data that this config does not have
// (if you want perfect equality, do A.CompareEqual(B) && B.CompareEqual(A))
struct Comparison
{
Comparison(const std::string &Section, const std::string &Variable, const FString &Expected,
const FString &Other)
: SectionName(Section), VariableName(Variable), ThisValue(Expected), OtherValue(Other)
{
}
Comparison(const std::string &Section, const std::string &Variable)
: SectionName(Section), VariableName(Variable), bIsMissing(true)
{
}
const std::string SectionName, VariableName;
const FString ThisValue, OtherValue;
const bool bIsMissing = false;
};
std::vector<Comparison> Diff = {};
for (const auto &SectionData : Sections)
{
const std::string &SectionName = SectionData.first;
const IniSection &Section = SectionData.second;
for (const auto &EntryData : Section.Entries)
{
const std::string &VariableName = EntryData.first;
const ParamString &Value = EntryData.second;
ParamString OtherValue;
if (Other.Find(SectionName, VariableName, OtherValue))
{
// compare equality
if (!Value.DataStr.Equals(OtherValue.DataStr, ESearchCase::IgnoreCase))
{
Diff.push_back({SectionName, VariableName, Value.DataStr, OtherValue.DataStr});
}
}
else // did not find, missing
{
Diff.push_back({SectionName, VariableName});
}
}
}
bool bIsDifferent = (Diff.size() > 0);
// print differences
if (bPrintWarning && bIsDifferent)
{
LOG_WARN("Found config differences this {\"%s\"} and Other {\"%s\"}", *FilePath, *Other.FilePath);
for (const Comparison &Comp : Diff)
{
if (Comp.bIsMissing)
{
LOG_WARN("Missing [%s] \"%s\"", *FString(Comp.SectionName.c_str()),
*FString(Comp.VariableName.c_str()));
}
else
{
LOG_WARN("This [%s] \"%s\" {%s} does not match {%s}", *FString(Comp.SectionName.c_str()),
*FString(Comp.VariableName.c_str()), *Comp.ThisValue, *Comp.OtherValue);
}
}
}
return !bIsDifferent;
}
void Insert(const ConfigFile &Other)
{
if (!Other.bIsValid())
return;
FilePath += ";" + Other.FilePath;
if (Other.Sections.size() > 0)
Sections.insert(Other.Sections.begin(), Other.Sections.end());
bSuccessfulUpdate = true;
}
static ConfigFile Import(const std::string &Configuration)
{
// takes a flattened INI configuration file as parameter and reads it into a ConfigFile class
return ConfigFile(Configuration);
}
std::string Export() const
{
std::ostringstream oss;
oss << std::endl
<< "# This is an exported config file originally from \"" << TCHAR_TO_UTF8(*FilePath) << "\"" << std::endl
<< std::endl;
for (const auto &SectionData : Sections)
{
const std::string &SectionName = SectionData.first;
const IniSection &Section = SectionData.second;
oss << "[" << SectionName << "]" << std::endl; // The overarching section first
for (const auto &EntryData : Section.Entries)
{
const std::string &VariableName = EntryData.first;
const ParamString &Data = EntryData.second;
const std::string DataUTF = TCHAR_TO_UTF8(*Data.DataStr); // convert to UTF-8 format
const std::string DataStdStr = Data.bHasQuotes ? "\"" + DataUTF + "\"" : DataUTF;
oss << VariableName << "=" << DataStdStr << std::endl;
}
oss << std::endl;
}
return oss.str();
}
private:
bool ReadFile(bool bVerbose)
{
check(!FilePath.IsEmpty());
if (bVerbose)
{
LOG("Reading config from %s", *FilePath);
}
std::ifstream MatchingFile(TCHAR_TO_ANSI(*FilePath), std::ios::in);
if (MatchingFile)
{
return Update(MatchingFile);
}
LOG_ERROR("Unable to open the config file \"%s\"", *FilePath);
return false;
}
bool Update(std::istream &InputStream) // reload the internally tracked table of params
{
/// performs a single pass over the config stream to collect all variables into Params
std::string Line;
std::string Section = "";
while (std::getline(InputStream, Line))
{
if (InputStream.bad()) // IO error
return false;
// std::string stdKey = std::string(TCHAR_TO_UTF8(*Key));
if (Line[0] == '#' || Line[0] == ';') // ignore comments
continue;
std::istringstream iss_Line(Line);
if (Line[0] == '[') // test section
{
std::getline(iss_Line, Section, ']');
Section = Section.substr(1); // skip leading '['
continue;
}
std::string Key;
if (std::getline(iss_Line, Key, '=')) // gets left side of '=' into FileKey
{
std::string Value;
if (std::getline(iss_Line, Value, '#')) // gets left side of '#' for comments
{
// ensure there is a section (create one if necesary) to store this key:value pair
if (Sections.find(Section) == Sections.end())
{
Sections.insert({Section, IniSection(Section)});
}
check(Sections.find(Section) != Sections.end());
auto &CorrespondingSection = Sections.find(Section)->second;
CorrespondingSection.Entries.insert({Key, ParamString(Value)});
}
}
}
return true;
}
private:
struct ParamString
{
ParamString() = default;
ParamString(const std::string &Value)
{
DataStr = FString(Value.c_str()).TrimStartAndEnd().TrimQuotes(&bHasQuotes);
}
FString DataStr = ""; // string representation of the data to parse into primitives
template <typename T> inline T DecipherToType() const
{
// supports FVector, FVector2D, FLinearColor, FQuat, and FRotator,
// basically any UE4 type that has a ::InitFromString method
T Ret;
if (Ret.InitFromString(DataStr) == false)
{
LOG_ERROR("Unable to decipher \"%s\" to a type", *DataStr);
}
return Ret;
}
template <> inline bool DecipherToType<bool>() const
{
return DataStr.ToBool();
}
template <> inline int DecipherToType<int>() const
{
return FCString::Atoi(*DataStr);
}
template <> inline float DecipherToType<float>() const
{
return FCString::Atof(*DataStr);
}
template <> inline FString DecipherToType<FString>() const
{
return DataStr;
}
template <> inline FName DecipherToType<FName>() const
{
return FName(*DataStr);
}
bool bHasQuotes = false;
};
struct IniSection
{
IniSection(const std::string &SectionName) : SectionHeader(SectionName)
{
}
std::string SectionHeader; // typically what is contained in [Sections]
std::unordered_map<std::string, ParamString> Entries; // everything else
};
private:
// construct a ConfigFile by passing in the entire contents of the config file (rather than the path to read)
ConfigFile(const std::string &ConfigurationContents) : FilePath("")
{
std::istringstream iss(ConfigurationContents);
bSuccessfulUpdate = Update(iss);
}
// using std::string variant for internal use (FString for user-facing)
template <typename T> bool GetValue(const std::string &SectionName, const std::string &VariableName, T &Value) const
{
ParamString Param;
bool bFound = Find(SectionName, VariableName, Param);
if (bFound)
Value = Param.DecipherToType<T>();
return bFound;
}
bool Find(const std::string &SectionName, const std::string &VariableName, ParamString &Out) const
{
auto SectionIt = Sections.find(SectionName);
if (SectionIt == Sections.end())
{
LOG_ERROR("No section in config file matches \"%s\"", *FString(SectionName.c_str()));
return false;
}
const IniSection &Section = SectionIt->second;
auto EntryIt = Section.Entries.find(VariableName);
if (EntryIt == Section.Entries.end())
{
LOG_ERROR("No entry for \"%s\" in [%s] section found!", *FString(VariableName.c_str()),
*FString(SectionName.c_str()));
return false;
}
Out = EntryIt->second;
// enable this for debug purposes
// LOG("Read [%s]\"%s\" -> %s", *FString(SectionName.c_str()), *FString(VariableName.c_str()), *Out.DataStr);
return true; // found successfully!
}
private:
FString FilePath; // const except for overwrite
bool bSuccessfulUpdate = false;
std::unordered_map<std::string, IniSection> Sections;
};
static ConfigFile GeneralParams(FPaths::Combine(CarlaUE4Path, TEXT("Config/DReyeVRConfig.ini")));