#include "config.h" #include #include #include #include #include #include #include "db.h" #include "memory.h" #include "platform_compat.h" namespace fallout { #define CONFIG_FILE_MAX_LINE_LENGTH (256) // The initial number of sections (or key-value) pairs in the config. #define CONFIG_INITIAL_CAPACITY (10) static bool configParseLine(Config* config, char* string); static bool configParseKeyValue(char* string, char* key, char* value); static bool configEnsureSectionExists(Config* config, const char* sectionKey); static bool configTrimString(char* string); // Last section key read from .INI file. // // 0x518224 static char gConfigLastSectionKey[CONFIG_FILE_MAX_LINE_LENGTH] = "unknown"; // 0x42BD90 bool configInit(Config* config) { if (config == NULL) { return false; } if (dictionaryInit(config, CONFIG_INITIAL_CAPACITY, sizeof(ConfigSection), NULL) != 0) { return false; } return true; } // 0x42BDBC void configFree(Config* config) { if (config == NULL) { return; } for (int sectionIndex = 0; sectionIndex < config->entriesLength; sectionIndex++) { DictionaryEntry* sectionEntry = &(config->entries[sectionIndex]); ConfigSection* section = (ConfigSection*)sectionEntry->value; for (int keyValueIndex = 0; keyValueIndex < section->entriesLength; keyValueIndex++) { DictionaryEntry* keyValueEntry = &(section->entries[keyValueIndex]); char** value = (char**)keyValueEntry->value; internal_free(*value); *value = NULL; } dictionaryFree(section); } dictionaryFree(config); } // Parses command line argments and adds them into the config. // // The expected format of [argv] elements are "[section]key=value", otherwise // the element is silently ignored. // // NOTE: This function trims whitespace in key-value pair, but not in section. // I don't know if this is intentional or it's bug. // // 0x42BE38 bool configParseCommandLineArguments(Config* config, int argc, char** argv) { if (config == NULL) { return false; } for (int arg = 0; arg < argc; arg++) { char* pch; char* string = argv[arg]; // Find opening bracket. pch = strchr(string, '['); if (pch == NULL) { continue; } char* sectionKey = pch + 1; // Find closing bracket. pch = strchr(sectionKey, ']'); if (pch == NULL) { continue; } *pch = '\0'; char key[260]; char value[260]; if (configParseKeyValue(pch + 1, key, value)) { if (!configSetString(config, sectionKey, key, value)) { *pch = ']'; return false; } } *pch = ']'; } return true; } // 0x42BF48 bool configGetString(Config* config, const char* sectionKey, const char* key, char** valuePtr) { if (config == NULL || sectionKey == NULL || key == NULL || valuePtr == NULL) { return false; } int sectionIndex = dictionaryGetIndexByKey(config, sectionKey); if (sectionIndex == -1) { return false; } DictionaryEntry* sectionEntry = &(config->entries[sectionIndex]); ConfigSection* section = (ConfigSection*)sectionEntry->value; int index = dictionaryGetIndexByKey(section, key); if (index == -1) { return false; } DictionaryEntry* keyValueEntry = &(section->entries[index]); *valuePtr = *(char**)keyValueEntry->value; return true; } // 0x42BF90 bool configSetString(Config* config, const char* sectionKey, const char* key, const char* value) { if (config == NULL || sectionKey == NULL || key == NULL || value == NULL) { return false; } int sectionIndex = dictionaryGetIndexByKey(config, sectionKey); if (sectionIndex == -1) { if (!configEnsureSectionExists(config, sectionKey)) { return false; } sectionIndex = dictionaryGetIndexByKey(config, sectionKey); } DictionaryEntry* sectionEntry = &(config->entries[sectionIndex]); ConfigSection* section = (ConfigSection*)sectionEntry->value; int index = dictionaryGetIndexByKey(section, key); if (index != -1) { DictionaryEntry* keyValueEntry = &(section->entries[index]); char** existingValue = (char**)keyValueEntry->value; internal_free(*existingValue); *existingValue = NULL; dictionaryRemoveValue(section, key); } char* valueCopy = internal_strdup(value); if (valueCopy == NULL) { return false; } if (dictionaryAddValue(section, key, &valueCopy) == -1) { internal_free(valueCopy); return false; } return true; } // 0x42C05C bool configGetInt(Config* config, const char* sectionKey, const char* key, int* valuePtr, unsigned char base /* = 0 */) { if (valuePtr == NULL) { return false; } char* stringValue; if (!configGetString(config, sectionKey, key, &stringValue)) { return false; } char* end; errno = 0; long l = strtol(stringValue, &end, base); // see https://stackoverflow.com/a/6154614 // The link above says right things about converting strings to numbers, // however we need to maintain compatibility with atoi implementation and // original game data. One example of the problem is worldmap.txt where // frequency values expressed as percentages (Frequent=38%). If we handle // the result like the link above suggests (and what previous implementation // provided), we'll simply end up returning `false`, since there will be // unconverted characters left. On the other hand, this function is also // used to parse Sfall config values, which uses hexadecimal notation to // represent colors. We're not going to need any of these in the long run so // for now simply ignore any error that could arise during conversion. *valuePtr = l; return true; } // 0x42C090 bool configGetIntList(Config* config, const char* sectionKey, const char* key, int* arr, int count) { if (arr == NULL || count < 2) { return false; } char* string; if (!configGetString(config, sectionKey, key, &string)) { return false; } char temp[CONFIG_FILE_MAX_LINE_LENGTH]; string = strncpy(temp, string, CONFIG_FILE_MAX_LINE_LENGTH - 1); while (1) { char* pch = strchr(string, ','); if (pch == NULL) { break; } count--; if (count == 0) { break; } *pch = '\0'; *arr++ = atoi(string); string = pch + 1; } // SFALL: Fix getting last item in a list if the list has less than the // requested number of values (for `chem_primary_desire`). if (count > 0) { *arr = atoi(string); count--; } return count == 0; } // 0x42C160 bool configSetInt(Config* config, const char* sectionKey, const char* key, int value) { char stringValue[20]; compat_itoa(value, stringValue, 10); return configSetString(config, sectionKey, key, stringValue); } // Reads .INI file into config. // // 0x42C280 bool configRead(Config* config, const char* filePath, bool isDb) { if (config == NULL || filePath == NULL) { return false; } char string[CONFIG_FILE_MAX_LINE_LENGTH]; if (isDb) { File* stream = fileOpen(filePath, "rb"); if (stream != NULL) { while (fileReadString(string, sizeof(string), stream) != NULL) { configParseLine(config, string); } fileClose(stream); } } else { FILE* stream = compat_fopen(filePath, "rt"); if (stream != NULL) { while (compat_fgets(string, sizeof(string), stream) != NULL) { configParseLine(config, string); } fclose(stream); } // FIXME: This function returns `true` even if the file was not actually // read. I'm pretty sure it's bug. } return true; } // Writes config into .INI file. // // 0x42C324 bool configWrite(Config* config, const char* filePath, bool isDb) { if (config == NULL || filePath == NULL) { return false; } if (isDb) { File* stream = fileOpen(filePath, "wt"); if (stream == NULL) { return false; } for (int sectionIndex = 0; sectionIndex < config->entriesLength; sectionIndex++) { DictionaryEntry* sectionEntry = &(config->entries[sectionIndex]); filePrintFormatted(stream, "[%s]\n", sectionEntry->key); ConfigSection* section = (ConfigSection*)sectionEntry->value; for (int index = 0; index < section->entriesLength; index++) { DictionaryEntry* keyValueEntry = &(section->entries[index]); filePrintFormatted(stream, "%s=%s\n", keyValueEntry->key, *(char**)keyValueEntry->value); } filePrintFormatted(stream, "\n"); } fileClose(stream); } else { FILE* stream = compat_fopen(filePath, "wt"); if (stream == NULL) { return false; } for (int sectionIndex = 0; sectionIndex < config->entriesLength; sectionIndex++) { DictionaryEntry* sectionEntry = &(config->entries[sectionIndex]); fprintf(stream, "[%s]\n", sectionEntry->key); ConfigSection* section = (ConfigSection*)sectionEntry->value; for (int index = 0; index < section->entriesLength; index++) { DictionaryEntry* keyValueEntry = &(section->entries[index]); fprintf(stream, "%s=%s\n", keyValueEntry->key, *(char**)keyValueEntry->value); } fprintf(stream, "\n"); } fclose(stream); } return true; } // Parses a line from .INI file into config. // // A line either contains a "[section]" section key or "key=value" pair. In the // first case section key is not added to config immediately, instead it is // stored in [gConfigLastSectionKey] for later usage. This prevents empty // sections in the config. // // In case of key-value pair it pretty straight forward - it adds key-value // pair into previously read section key stored in [gConfigLastSectionKey]. // // Returns `true` when a section was parsed or key-value pair was parsed and // added to the config, or `false` otherwise. // // 0x42C4BC static bool configParseLine(Config* config, char* string) { char* pch; // Find comment marker and truncate the string. pch = strchr(string, ';'); if (pch != NULL) { *pch = '\0'; } // CE: Original implementation treats any line with brackets as section key. // The problem can be seen when loading Olympus settings (ddraw.ini), which // contains the following line: // // ```ini // VersionString=Olympus 2207 [Complete]. // ``` // // It thinks that [Complete] is a start of new section, and puts remaining // keys there. // Skip leading whitespace. while (isspace(*string)) { string++; } // Check if it's a section key. if (*string == '[') { char* sectionKey = string + 1; // Find closing bracket. pch = strchr(sectionKey, ']'); if (pch != NULL) { *pch = '\0'; strcpy(gConfigLastSectionKey, sectionKey); return configTrimString(gConfigLastSectionKey); } } char key[260]; char value[260]; if (!configParseKeyValue(string, key, value)) { return false; } return configSetString(config, gConfigLastSectionKey, key, value); } // Splits "key=value" pair from [string] and copy appropriate parts into [key] // and [value] respectively. // // Both key and value are trimmed. // // 0x42C594 static bool configParseKeyValue(char* string, char* key, char* value) { if (string == NULL || key == NULL || value == NULL) { return false; } // Find equals character. char* pch = strchr(string, '='); if (pch == NULL) { return false; } *pch = '\0'; strcpy(key, string); strcpy(value, pch + 1); *pch = '='; configTrimString(key); configTrimString(value); return true; } // Ensures the config has a section with specified key. // // Return `true` if section exists or it was successfully added, or `false` // otherwise. // // 0x42C638 static bool configEnsureSectionExists(Config* config, const char* sectionKey) { if (config == NULL || sectionKey == NULL) { return false; } if (dictionaryGetIndexByKey(config, sectionKey) != -1) { // Section already exists, no need to do anything. return true; } ConfigSection section; if (dictionaryInit(§ion, CONFIG_INITIAL_CAPACITY, sizeof(char**), NULL) == -1) { return false; } if (dictionaryAddValue(config, sectionKey, §ion) == -1) { return false; } return true; } // Removes leading and trailing whitespace from the specified string. // // 0x42C698 static bool configTrimString(char* string) { if (string == NULL) { return false; } size_t length = strlen(string); if (length == 0) { return true; } // Starting from the end of the string, loop while it's a whitespace and // decrement string length. char* pch = string + length - 1; while (length != 0 && isspace(*pch)) { length--; pch--; } // pch now points to the last non-whitespace character. pch[1] = '\0'; // Starting from the beginning of the string loop while it's a whitespace // and decrement string length. pch = string; while (isspace(*pch)) { pch++; length--; } // pch now points for to the first non-whitespace character. memmove(string, pch, length + 1); return true; } // 0x42C718 bool configGetDouble(Config* config, const char* sectionKey, const char* key, double* valuePtr) { if (valuePtr == NULL) { return false; } char* stringValue; if (!configGetString(config, sectionKey, key, &stringValue)) { return false; } *valuePtr = strtod(stringValue, NULL); return true; } // 0x42C74C bool configSetDouble(Config* config, const char* sectionKey, const char* key, double value) { char stringValue[32]; snprintf(stringValue, sizeof(stringValue), "%.6f", value); return configSetString(config, sectionKey, key, stringValue); } // NOTE: Boolean-typed variant of [configGetInt]. bool configGetBool(Config* config, const char* sectionKey, const char* key, bool* valuePtr) { if (valuePtr == NULL) { return false; } int integerValue; if (!configGetInt(config, sectionKey, key, &integerValue)) { return false; } *valuePtr = integerValue != 0; return true; } // NOTE: Boolean-typed variant of [configGetInt]. bool configSetBool(Config* config, const char* sectionKey, const char* key, bool value) { return configSetInt(config, sectionKey, key, value ? 1 : 0); } } // namespace fallout