From bd70fc2322ac9d513d137eaa5e50da613bec421b Mon Sep 17 00:00:00 2001
From: Alexander Batalov <alex.batalov@gmail.com>
Date: Tue, 30 May 2023 12:21:08 +0300
Subject: [PATCH] Add ini opcodes

---
 CMakeLists.txt       |   2 +
 src/config.cc        |  28 +++++----
 src/game.cc          |   5 ++
 src/sfall_config.cc  |   2 +
 src/sfall_config.h   |   2 +
 src/sfall_ini.cc     | 146 +++++++++++++++++++++++++++++++++++++++++++
 src/sfall_ini.h      |  17 +++++
 src/sfall_opcodes.cc |  29 +++++++++
 8 files changed, 219 insertions(+), 12 deletions(-)
 create mode 100644 src/sfall_ini.cc
 create mode 100644 src/sfall_ini.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1f3bd73..6eb6375 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -267,6 +267,8 @@ target_sources(${EXECUTABLE_NAME} PUBLIC
     "src/sfall_config.h"
     "src/sfall_global_vars.cc"
     "src/sfall_global_vars.h"
+    "src/sfall_ini.cc"
+    "src/sfall_ini.h"
     "src/sfall_lists.cc"
     "src/sfall_lists.h"
     "src/sfall_opcodes.cc"
diff --git a/src/config.cc b/src/config.cc
index 966b0d4..2ec1aad 100644
--- a/src/config.cc
+++ b/src/config.cc
@@ -280,24 +280,28 @@ bool configRead(Config* config, const char* filePath, bool isDb)
 
     if (isDb) {
         File* stream = fileOpen(filePath, "rb");
-        if (stream != NULL) {
-            while (fileReadString(string, sizeof(string), stream) != NULL) {
-                configParseLine(config, string);
-            }
-            fileClose(stream);
+
+        // CE: Return `false` if file does not exists in database.
+        if (stream == NULL) {
+            return false;
         }
+
+        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);
+        // CE: Return `false` if file does not exists on the file system.
+        if (stream == NULL) {
+            return false;
         }
 
-        // FIXME: This function returns `true` even if the file was not actually
-        // read. I'm pretty sure it's bug.
+        while (compat_fgets(string, sizeof(string), stream) != NULL) {
+            configParseLine(config, string);
+        }
+        fclose(stream);
     }
 
     return true;
diff --git a/src/game.cc b/src/game.cc
index b7bfe0a..4412024 100644
--- a/src/game.cc
+++ b/src/game.cc
@@ -53,6 +53,7 @@
 #include "sfall_arrays.h"
 #include "sfall_config.h"
 #include "sfall_global_vars.h"
+#include "sfall_ini.h"
 #include "sfall_lists.h"
 #include "skill.h"
 #include "skilldex.h"
@@ -354,6 +355,10 @@ int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int a4
         return -1;
     }
 
+    char* customConfigBasePath;
+    configGetString(&gSfallConfig, SFALL_CONFIG_SCRIPTS_KEY, SFALL_CONFIG_INI_CONFIG_FOLDER, &customConfigBasePath);
+    sfall_ini_set_base_path(customConfigBasePath);
+
     messageListRepositorySetStandardMessageList(STANDARD_MESSAGE_LIST_MISC, &gMiscMessageList);
 
     return 0;
diff --git a/src/sfall_config.cc b/src/sfall_config.cc
index c0f4b6c..c38b825 100644
--- a/src/sfall_config.cc
+++ b/src/sfall_config.cc
@@ -58,6 +58,8 @@ bool sfallConfigInit(int argc, char** argv)
     configSetInt(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_MOVIE_TIMER_ARTIMER3, 270);
     configSetInt(&gSfallConfig, SFALL_CONFIG_MISC_KEY, SFALL_CONFIG_MOVIE_TIMER_ARTIMER4, 360);
 
+    configSetString(&gSfallConfig, SFALL_CONFIG_SCRIPTS_KEY, SFALL_CONFIG_INI_CONFIG_FOLDER, "");
+
     char path[COMPAT_MAX_PATH];
     char* executable = argv[0];
     char* ch = strrchr(executable, '\\');
diff --git a/src/sfall_config.h b/src/sfall_config.h
index 86aa7a7..f165450 100644
--- a/src/sfall_config.h
+++ b/src/sfall_config.h
@@ -8,6 +8,7 @@ namespace fallout {
 #define SFALL_CONFIG_FILE_NAME "ddraw.ini"
 
 #define SFALL_CONFIG_MISC_KEY "Misc"
+#define SFALL_CONFIG_SCRIPTS_KEY "Scripts"
 
 #define SFALL_CONFIG_DUDE_NATIVE_LOOK_JUMPSUIT_MALE_KEY "MaleDefaultModel"
 #define SFALL_CONFIG_DUDE_NATIVE_LOOK_JUMPSUIT_FEMALE_KEY "FemaleDefaultModel"
@@ -68,6 +69,7 @@ namespace fallout {
 #define SFALL_CONFIG_TOWN_MAP_HOTKEYS_FIX_KEY "TownMapHotkeysFix"
 #define SFALL_CONFIG_EXTRA_MESSAGE_LISTS_KEY "ExtraGameMsgFileList"
 #define SFALL_CONFIG_NUMBERS_IS_DIALOG_KEY "NumbersInDialogue"
+#define SFALL_CONFIG_INI_CONFIG_FOLDER "IniConfigFolder"
 
 #define SFALL_CONFIG_BURST_MOD_DEFAULT_CENTER_MULTIPLIER 1
 #define SFALL_CONFIG_BURST_MOD_DEFAULT_CENTER_DIVISOR 3
diff --git a/src/sfall_ini.cc b/src/sfall_ini.cc
new file mode 100644
index 0000000..0347a3b
--- /dev/null
+++ b/src/sfall_ini.cc
@@ -0,0 +1,146 @@
+#include "sfall_ini.h"
+
+#include <algorithm>
+#include <cstring>
+
+#include "config.h"
+#include "platform_compat.h"
+
+namespace fallout {
+
+/// The max length of `fileName` chunk in the triplet.
+static constexpr size_t kFileNameMaxSize = 63;
+
+/// The max length of `section` chunk in the triplet.
+static constexpr size_t kSectionMaxSize = 32;
+
+/// Special .ini file names which are accessed without adding base path.
+static constexpr char* kSystemConfigFileNames[] = {
+    "ddraw.ini",
+    "f2_res.ini",
+};
+
+static char basePath[COMPAT_MAX_PATH];
+
+/// Parses "fileName|section|key" triplet into parts. `fileName` and `section`
+/// chunks are copied into appropriate variables. Returns the pointer to `key`,
+/// or `nullptr` on any error.
+static const char* parse_ini_triplet(const char* triplet, char* fileName, char* section)
+{
+    const char* fileNameSectionSep = strchr(triplet, '|');
+    if (fileNameSectionSep == nullptr) {
+        return nullptr;
+    }
+
+    size_t fileNameLength = fileNameSectionSep - triplet;
+    if (fileNameLength > kFileNameMaxSize) {
+        return nullptr;
+    }
+
+    const char* sectionKeySep = strchr(fileNameSectionSep + 1, '|');
+    if (sectionKeySep == nullptr) {
+        return nullptr;
+    }
+
+    size_t sectionLength = sectionKeySep - fileNameSectionSep - 1;
+    if (sectionLength > kSectionMaxSize) {
+        return nullptr;
+    }
+
+    strncpy(fileName, triplet, fileNameLength);
+    fileName[fileNameLength] = '\0';
+
+    strncpy(section, fileNameSectionSep + 1, sectionLength);
+    section[sectionLength] = '\0';
+
+    return sectionKeySep + 1;
+}
+
+/// Returns `true` if given `fileName` is a special system .ini file name.
+static bool is_system_file_name(const char* fileName)
+{
+    for (auto& systemFileName : kSystemConfigFileNames) {
+        if (compat_stricmp(systemFileName, fileName) == 0) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void sfall_ini_set_base_path(const char* path)
+{
+    if (path != nullptr) {
+        strcpy(basePath, path);
+
+        size_t length = strlen(basePath);
+        if (length > 0) {
+            if (basePath[length - 1] == '\\' || basePath[length - 1] == '/') {
+                basePath[length - 1] = '\0';
+            }
+        }
+    } else {
+        basePath[0] = '\0';
+    }
+}
+
+bool sfall_ini_get_int(const char* triplet, int* value)
+{
+    char string[20];
+    if (!sfall_ini_get_string(triplet, string, sizeof(string))) {
+        return false;
+    }
+
+    *value = atol(string);
+
+    return true;
+}
+
+bool sfall_ini_get_string(const char* triplet, char* value, size_t size)
+{
+    char fileName[kFileNameMaxSize];
+    char section[kSectionMaxSize];
+
+    const char* key = parse_ini_triplet(triplet, fileName, section);
+    if (key == nullptr) {
+        return false;
+    }
+
+    Config config;
+    if (!configInit(&config)) {
+        return false;
+    }
+
+    char path[COMPAT_MAX_PATH];
+    bool loaded = false;
+
+    if (basePath[0] != '\0' && !is_system_file_name(fileName)) {
+        // Attempt to load requested file in base directory.
+        snprintf(path, sizeof(path), "%s\\%s", basePath, fileName);
+        loaded = configRead(&config, path, false);
+    }
+
+    if (!loaded) {
+        // There was no base path set, requested file is a system config, or
+        // non-system config file was not found the base path - attempt to load
+        // from current working directory.
+        strcpy(path, fileName);
+        loaded = configRead(&config, path, false);
+    }
+
+    bool ok = false;
+    if (loaded) {
+        char* stringValue;
+        if (configGetString(&config, section, key, &stringValue)) {
+            strncpy(value, stringValue, size - 1);
+            value[size - 1] = '\0';
+            ok = true;
+        }
+    }
+
+    configFree(&config);
+
+    return ok;
+}
+
+} // namespace fallout
diff --git a/src/sfall_ini.h b/src/sfall_ini.h
new file mode 100644
index 0000000..793b853
--- /dev/null
+++ b/src/sfall_ini.h
@@ -0,0 +1,17 @@
+#ifndef FALLOUT_SFALL_INI_H_
+#define FALLOUT_SFALL_INI_H_
+
+namespace fallout {
+
+/// Sets base directory to lookup .ini files.
+void sfall_ini_set_base_path(const char* path);
+
+/// Reads integer key identified by "fileName|section|key" triplet into `value`.
+bool sfall_ini_get_int(const char* triplet, int* value);
+
+/// Reads string key identified by "fileName|section|key" triplet into `value`.
+bool sfall_ini_get_string(const char* triplet, char* value, size_t size);
+
+} // namespace fallout
+
+#endif /* FALLOUT_SFALL_INI_H_ */
diff --git a/src/sfall_opcodes.cc b/src/sfall_opcodes.cc
index f6f3ede..3b101ce 100644
--- a/src/sfall_opcodes.cc
+++ b/src/sfall_opcodes.cc
@@ -19,6 +19,7 @@
 #include "scripts.h"
 #include "sfall_arrays.h"
 #include "sfall_global_vars.h"
+#include "sfall_ini.h"
 #include "sfall_lists.h"
 #include "stat.h"
 #include "svga.h"
@@ -147,6 +148,19 @@ static void opGetGlobalInt(Program* program)
     programStackPushInteger(program, value);
 }
 
+// get_ini_setting
+static void op_get_ini_setting(Program* program)
+{
+    const char* string = programStackPopString(program);
+
+    int value;
+    if (sfall_ini_get_int(string, &value)) {
+        programStackPushInteger(program, value);
+    } else {
+        programStackPushInteger(program, -1);
+    }
+}
+
 // get_game_mode
 static void opGetGameMode(Program* program)
 {
@@ -181,6 +195,19 @@ static void op_set_bodypart_hit_modifier(Program* program)
     combat_set_hit_location_penalty(hit_location, penalty);
 }
 
+// get_ini_string
+static void op_get_ini_string(Program* program)
+{
+    const char* string = programStackPopString(program);
+
+    char value[256];
+    if (sfall_ini_get_string(string, value, sizeof(value))) {
+        programStackPushString(program, value);
+    } else {
+        programStackPushInteger(program, -1);
+    }
+}
+
 // sqrt
 static void op_sqrt(Program* program)
 {
@@ -785,11 +812,13 @@ void sfallOpcodesInit()
     interpreterRegisterOpcode(0x8193, opGetCurrentHand);
     interpreterRegisterOpcode(0x819D, opSetGlobalVar);
     interpreterRegisterOpcode(0x819E, opGetGlobalInt);
+    interpreterRegisterOpcode(0x81AC, op_get_ini_setting);
     interpreterRegisterOpcode(0x81AF, opGetGameMode);
     interpreterRegisterOpcode(0x81B3, op_get_uptime);
     interpreterRegisterOpcode(0x81B6, op_set_car_current_town);
     interpreterRegisterOpcode(0x81DF, op_get_bodypart_hit_modifier);
     interpreterRegisterOpcode(0x81E0, op_set_bodypart_hit_modifier);
+    interpreterRegisterOpcode(0x81EB, op_get_ini_string);
     interpreterRegisterOpcode(0x81EC, op_sqrt);
     interpreterRegisterOpcode(0x81ED, op_abs);
     interpreterRegisterOpcode(0x8204, op_get_proto_data);