From 437544ca0e0b6fd692fd256ef22809474740389d Mon Sep 17 00:00:00 2001 From: Xeno Fox Date: Fri, 21 Aug 2026 12:30:23 +1000 Subject: [PATCH 01/11] feat: initial implementation of the config manager --- CMakeLists.txt | 8 +- include/xtrpg/config/ConfigManager.hpp | 238 +++++++++++++++++++++++++ src/config/ConfigManager.cpp | 190 ++++++++++++++++++++ 3 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 include/xtrpg/config/ConfigManager.hpp create mode 100644 src/config/ConfigManager.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f4d5f40..23df9d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,9 +87,15 @@ if(UserModules_FOUND) endforeach() endif() +# Config Server Target +add_library(xtrpg_config STATIC + src/config/ConfigManager.cpp +) +target_include_directories(xtrpg_config PUBLIC include) + # Executable Target add_executable(xtrpg_cpp_server apps/main.cpp) -# target_link_libraries(xtrpg_cpp_server PRIVATE xtrpg_core) +target_link_libraries(xtrpg_cpp_server PRIVATE xtrpg_config) target_include_directories(xtrpg_cpp_server PRIVATE ${CMAKE_BINARY_DIR}/generated) # If modules produce targets registered via xmpp_register_user_module diff --git a/include/xtrpg/config/ConfigManager.hpp b/include/xtrpg/config/ConfigManager.hpp new file mode 100644 index 0000000..2754bf9 --- /dev/null +++ b/include/xtrpg/config/ConfigManager.hpp @@ -0,0 +1,238 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define REGISTER_MODULE_CONFIG(ModuleClass) \ + static struct ModuleClass##ConfigRegistrar { \ + ModuleClass##ConfigRegistrar() { \ + xtrpg::config::ConfigManager::registerStaticModule( \ + []() { return std::make_unique(); }); \ + } \ + } global_##ModuleClass##_config_registrar; + +namespace xtrpg::config { + +/** + * Supporting basic primitive types. + */ +using ConfigValue = std::variant; + +/** + * Represents a single configuration option with its metadata. + */ +struct ConfigOption { + /** + * Key name for the option, used in TOML and CLI (e.g., "threads" or + * "engine.render_quality"). + */ + std::string key; + + /** + * Default value for the option, used if not overridden by TOML or CLI. + */ + ConfigValue defaultValue; + + /** + * Description of the option, used for help output and documentation. + */ + std::string description; + + /** + * Optional CLI flag for the option, used for command-line overrides (e.g., + * "--threads"). + */ + std::string cliFlag; +}; + +/** + * Represents a module's configuration schema, including its name and the + * options it provides. + */ +struct ModuleConfig { + /** + * Name of the module, used as a section header in TOML and CLI (e.g., + * "engine" or "network"). + */ + std::string name; + + /** + * Optional description of the module, used for help output and documentation. + */ + std::string description; + + /** + * List of configuration options provided by the module, including their + * keys, default values, descriptions, and optional CLI flags. + */ + std::vector options; +}; + +/** + * Interface for modules to provide their configuration schema to the + * ConfigManager. + */ +class IModuleConfigProvider { +public: + /** + * Virtual destructor to ensure proper cleanup of derived classes. + */ + virtual ~IModuleConfigProvider() = default; + + /** + * Returns the configuration schema for the module, including its name and + * the options it provides. + * + * @returns A ModuleConfig object containing the module's name and its + * configuration options. + */ + virtual ModuleConfig getConfigSchema() const = 0; +}; + +/** + * Manages the configuration system, including loading from TOML files, + * parsing command-line arguments, and providing access to resolved values. + */ +class ConfigManager { +private: + /** + * Stores the resolved configuration values, organized by section and key. + */ + std::unordered_map> + m_values; + + /** + * Stores the configuration schemas for registered modules, organized by + * section name. + */ + std::unordered_map> m_schemas; + + /** + * Maps CLI flags to their corresponding section.key for easy lookup during + * command-line parsing. --flag -> "section.key" + */ + std::unordered_map m_cliFlagMap; + + /** + * Formats a ConfigValue into a string representation for display in help + * output and documentation. + * + * @param val The ConfigValue to format. + * @returns A string representation of the value, suitable for display. + */ + std::string formatValue(const ConfigValue &val) const { + return std::visit( + [](auto &&arg) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) + return arg ? "true" : "false"; + else if constexpr (std::is_same_v) + return "\"" + arg + "\""; + else + return std::to_string(arg); + }, + val); + } + +public: + /** + * Type alias for a factory function that creates an instance of + * IModuleConfigProvider. + */ + using ProviderFactory = + std::function()>; + + /** + * Returns a reference to the static registry of module configuration provider + * factories, allowing modules to register their configuration schemas at + * compile time. + */ + static std::vector &getRegistry() { + static std::vector registry; + return registry; + } + + /** + * Registers a module's configuration provider factory in the static registry, + * allowing it to be discovered and included in the final resolved + * configuration values. + * + * @param factory A factory function that returns a unique_ptr to an + * IModuleConfigProvider instance for the module. + */ + static void registerStaticModule(ProviderFactory factory) { + ConfigManager::getRegistry().push_back(factory); + } + + /** + * Registers all discovered modules from the static registry, allowing their + * configuration schemas to be included in the final resolved configuration + * values. This should be called after all modules have been registered and + * before loading any configuration files or parsing command-line arguments. + */ + void registerAllDiscoveredModules() { + for (const auto &factory : ConfigManager::getRegistry()) { + auto provider = factory(); + registerModule(*provider); + } + } + + /** + * Registers a module's configuration schema with the ConfigManager, allowing + * it to be included in the final resolved configuration values. + */ + void registerModule(const IModuleConfigProvider &provider); + + /** + * Prints a formatted help menu to the console, including usage instructions + * and descriptions of all registered configuration options. + * + * @param os The output stream to which the help menu will be printed. + * Defaults to std::cout. + */ + void printHelp(std::ostream &os) const; + + /** + * Loads configuration values from a TOML file, parsing the content and + * applying the values to the appropriate sections and keys. + */ + bool loadTomlFile(const std::string &fileContent); + + /** + * Parses command-line arguments, updating configuration values based on the + * provided flags and their associated values. + */ + void parseCLI(int argc, char *argv[]); + + /** + * Retrieves the resolved configuration value for a given section and key, + * returning it as the specified type T. Throws an exception if the section or + * key does not exist or if the type does not match. + */ + template + T get(const std::string §ion, const std::string &key) const { + return std::get(m_values.at(section).at(key)); + } + + /** + * Dumps the resolved configuration values to the specified output stream, + * formatting them in a human-readable way for inspection or debugging. + * + * @param os The output stream to which the resolved configuration will be + * dumped. Defaults to std::cout. + */ + void dumpResolvedConfig(std::ostream &os) const; +}; + +// inline std::ostream &operator<<(std::ostream &os, const ConfigValue &val) { +// std::visit([&os](auto &&arg) { os << arg; }, val); +// return os; +// } +} // namespace xtrpg::config \ No newline at end of file diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp new file mode 100644 index 0000000..2c1b7b4 --- /dev/null +++ b/src/config/ConfigManager.cpp @@ -0,0 +1,190 @@ +#include "xtrpg/config/ConfigManager.hpp" + +namespace xtrpg::config { + +/** + * Registers a module's configuration schema with the ConfigManager, allowing + * it to be included in the final resolved configuration values. + */ +void ConfigManager::registerModule(const IModuleConfigProvider &provider) { + ModuleConfig schema = provider.getConfigSchema(); + std::string section = schema.name; + + m_schemas[section] = schema.options; + + for (const auto &opt : schema.options) { + // Apply lowest priority layers first: Module / Platform Defaults + m_values[section][opt.key] = opt.defaultValue; + + if (!opt.cliFlag.empty()) { + m_cliFlagMap[opt.cliFlag] = section + "." + opt.key; + } + } +} + +/** + * Prints a formatted help menu to the console, including usage instructions + * and descriptions of all registered configuration options. + * + * @param os The output stream to which the help menu will be printed. + */ +void ConfigManager::printHelp(std::ostream &os) const { + os << "Usage: app [options]\n\n"; + for (const auto &[section, options] : m_schemas) { + os << "[" << section << "]\n"; + for (const auto &opt : options) { + os << " "; + if (!opt.cliFlag.empty()) { + os << opt.cliFlag << ", "; + } + os << "--" << section << "." << opt.key; + os << "\n " << opt.description; + os << " (Default: " << formatValue(opt.defaultValue) << ")\n"; + } + os << "\n"; + } +} + +/** + * Loads configuration values from a TOML file, parsing the content and + * applying the values to the appropriate sections and keys. + */ +bool ConfigManager::loadTomlFile(const std::string &fileContent) { + std::stringstream ss(fileContent); + std::string line; + std::string currentSection = "global"; + + while (std::getline(ss, line)) { + // Trim simple whitespace + line.erase(0, line.find_first_not_of(" \t\r\n")); + line.erase(line.find_last_not_of(" \t\r\n") + 1); + + if (line.empty() || line[0] == '#') + continue; + + // Section parsing [section] + if (line.front() == '[' && line.back() == ']') { + currentSection = line.substr(1, line.size() - 2); + continue; + } + + // Key-value parsing (key = value) + auto eqPos = line.find('='); + if (eqPos != std::string::npos) { + std::string key = line.substr(0, eqPos); + std::string valStr = line.substr(eqPos + 1); + + // Basic string clean up + key.erase(key.find_last_not_of(" \t") + 1); + valStr.erase(0, valStr.find_first_not_of(" \t")); + + // Deduce type & assign (Overwrites module/compile defaults) + if (valStr == "true") + // Boolean type set to true + m_values[currentSection][key] = true; + else if (valStr == "false") + // Boolean type set to false + m_values[currentSection][key] = false; + else if (valStr.front() == '"' && valStr.back() == '"') { + // String type + m_values[currentSection][key] = valStr.substr(1, valStr.size() - 2); + } else if (valStr.find('.') != std::string::npos) { + // Double type + m_values[currentSection][key] = std::stod(valStr); + } else { + // Integer type + m_values[currentSection][key] = std::stoll(valStr); + } + } + } + return true; +} + +/** + * Parses command-line arguments, updating configuration values based on the + * provided flags and their associated values. + */ +void ConfigManager::parseCLI(int argc, char *argv[]) { + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--help" || arg == "-h") { + printHelp(std::cout); + std::exit(0); + } + + // Check registered short/custom flags or dot-notation + // (--section.key=value) + std::string keyPath; + std::string valueStr; + + auto eqPos = arg.find('='); + if (eqPos != std::string::npos) { + keyPath = arg.substr(0, eqPos); + valueStr = arg.substr(eqPos + 1); + } else { + keyPath = arg; + if (i + 1 < argc && argv[i + 1][0] != '-') { + valueStr = argv[++i]; + } + } + + // Map short flag to section.key if applicable + if (m_cliFlagMap.count(keyPath)) { + keyPath = m_cliFlagMap[keyPath]; + } else if (keyPath.rfind("--", 0) == 0) { + keyPath = keyPath.substr(2); // Strip leading -- + } + + auto dotPos = keyPath.find('.'); + if (dotPos != std::string::npos) { + std::string section = keyPath.substr(0, dotPos); + std::string key = keyPath.substr(dotPos + 1); + + if (!valueStr.empty()) { + if (valueStr == "true") + m_values[section][key] = true; + else if (valueStr == "false") + m_values[section][key] = false; + else if (std::all_of(valueStr.begin(), valueStr.end(), ::isdigit)) { + m_values[section][key] = std::stoll(valueStr); + } else { + m_values[section][key] = valueStr; + } + } + } + } +} + +/** + * Dumps the resolved configuration values to the specified output stream, + * formatting them in a human-readable way for inspection or debugging. + * + * @param os The output stream to which the resolved configuration will be + * dumped. Defaults to std::cout. + */ +void ConfigManager::dumpResolvedConfig(std::ostream &os) const { + for (const auto &[section, options] : m_values) { + os << "[" << section << "]\n"; + for (const auto &[key, value] : options) { + + // output the key's documentation/description if available + auto schemaIt = m_schemas.find(section); + if (schemaIt != m_schemas.end()) { + const auto &schemaOptions = schemaIt->second; + auto optIt = std::find_if( + schemaOptions.begin(), schemaOptions.end(), + [&key](const ConfigOption &opt) { return opt.key == key; }); + if (optIt != schemaOptions.end()) { + os << " # " << optIt->description << "\n"; + } + } + + // output the key and its corresponding value in a readable format + os << key << " = " << formatValue(value) << "\n"; + } + os << "\n"; + } +} + +} // namespace xtrpg::config \ No newline at end of file From 8717146e4afaf7bf58aa859c41cb72d006f35cc3 Mon Sep 17 00:00:00 2001 From: Xeno Fox Date: Fri, 21 Aug 2026 12:46:44 +1000 Subject: [PATCH 02/11] fix: move toolchain to top of cmake file --- CMakeLists.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 23df9d8..15fe6b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,11 @@ cmake_minimum_required(VERSION 3.20) +# Auto-detect vcpkg if VCPKG_ROOT environment variable is set +if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) + set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + CACHE STRING "vcpkg toolchain file") +endif() + # If no variable is passed from the environment/command line, use a fallback if(NOT DEFINED BUILD_VERSION) set(BUILD_VERSION "0.1.0") @@ -16,12 +22,6 @@ project(xtrpg_cpp_server VERSION ${BUILD_VERSION} LANGUAGES CXX) # Generate the actual hpp file into the build binary directory configure_file("generated/version.hpp.in" ${CMAKE_BINARY_DIR}/generated/version.hpp) -# Auto-detect vcpkg if VCPKG_ROOT environment variable is set -if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) - set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" - CACHE STRING "vcpkg toolchain file") -endif() - set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) From c2d1a0554f79e2bbfff1abdd91d47485f00e875e Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 14:22:32 +1000 Subject: [PATCH 03/11] clean up processing commands --- apps/main.cpp | 12 +++++++++++- include/xtrpg/config/ConfigManager.hpp | 11 +---------- src/config/ConfigManager.cpp | 16 +++------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/apps/main.cpp b/apps/main.cpp index e0e347a..404571c 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -1,3 +1,13 @@ #include -int main() { std::cout << "Hello World" << std::endl; } \ No newline at end of file +#include "xtrpg/config/ConfigManager.hpp" + +int main(int argc, char *argv[]) { + + // Discover and register all modules' configuration schemas + xtrpg::config::ConfigManager configManager; + configManager.registerAllDiscoveredModules(); + configManager.parseCLI(argc, argv); + + return 0; +} \ No newline at end of file diff --git a/include/xtrpg/config/ConfigManager.hpp b/include/xtrpg/config/ConfigManager.hpp index 2754bf9..4443f98 100644 --- a/include/xtrpg/config/ConfigManager.hpp +++ b/include/xtrpg/config/ConfigManager.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -44,12 +45,6 @@ struct ConfigOption { * Description of the option, used for help output and documentation. */ std::string description; - - /** - * Optional CLI flag for the option, used for command-line overrides (e.g., - * "--threads"). - */ - std::string cliFlag; }; /** @@ -231,8 +226,4 @@ class ConfigManager { void dumpResolvedConfig(std::ostream &os) const; }; -// inline std::ostream &operator<<(std::ostream &os, const ConfigValue &val) { -// std::visit([&os](auto &&arg) { os << arg; }, val); -// return os; -// } } // namespace xtrpg::config \ No newline at end of file diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 2c1b7b4..64e546c 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -15,10 +15,6 @@ void ConfigManager::registerModule(const IModuleConfigProvider &provider) { for (const auto &opt : schema.options) { // Apply lowest priority layers first: Module / Platform Defaults m_values[section][opt.key] = opt.defaultValue; - - if (!opt.cliFlag.empty()) { - m_cliFlagMap[opt.cliFlag] = section + "." + opt.key; - } } } @@ -33,10 +29,6 @@ void ConfigManager::printHelp(std::ostream &os) const { for (const auto &[section, options] : m_schemas) { os << "[" << section << "]\n"; for (const auto &opt : options) { - os << " "; - if (!opt.cliFlag.empty()) { - os << opt.cliFlag << ", "; - } os << "--" << section << "." << opt.key; os << "\n " << opt.description; os << " (Default: " << formatValue(opt.defaultValue) << ")\n"; @@ -129,10 +121,7 @@ void ConfigManager::parseCLI(int argc, char *argv[]) { } } - // Map short flag to section.key if applicable - if (m_cliFlagMap.count(keyPath)) { - keyPath = m_cliFlagMap[keyPath]; - } else if (keyPath.rfind("--", 0) == 0) { + if (keyPath.rfind("--", 0) == 0) { keyPath = keyPath.substr(2); // Strip leading -- } @@ -164,6 +153,7 @@ void ConfigManager::parseCLI(int argc, char *argv[]) { * dumped. Defaults to std::cout. */ void ConfigManager::dumpResolvedConfig(std::ostream &os) const { + for (const auto &[section, options] : m_values) { os << "[" << section << "]\n"; for (const auto &[key, value] : options) { @@ -176,7 +166,7 @@ void ConfigManager::dumpResolvedConfig(std::ostream &os) const { schemaOptions.begin(), schemaOptions.end(), [&key](const ConfigOption &opt) { return opt.key == key; }); if (optIt != schemaOptions.end()) { - os << " # " << optIt->description << "\n"; + os << "# " << optIt->description << "\n"; } } From d53448df666ab3b0bf45b8763882721d3ff50225 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 14:23:02 +1000 Subject: [PATCH 04/11] update get method to return an optional --- include/xtrpg/config/ConfigManager.hpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/include/xtrpg/config/ConfigManager.hpp b/include/xtrpg/config/ConfigManager.hpp index 4443f98..59933fa 100644 --- a/include/xtrpg/config/ConfigManager.hpp +++ b/include/xtrpg/config/ConfigManager.hpp @@ -212,8 +212,19 @@ class ConfigManager { * key does not exist or if the type does not match. */ template - T get(const std::string §ion, const std::string &key) const { - return std::get(m_values.at(section).at(key)); + std::optional get(const std::string §ion, + const std::string &key) const { + auto sectionIt = m_values.find(section); + if (sectionIt == m_values.end()) { + return std::nullopt; + } + + auto keyIt = sectionIt->second.find(key); + if (keyIt == sectionIt->second.end()) { + return std::nullopt; + } + + return std::get(keyIt->second); } /** From 48487a1c3af80f33c732e3c957b805f5a0dc3d2e Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 14:59:14 +1000 Subject: [PATCH 05/11] Add layered config value resolution Configuration values now track system defaults, file overrides, and CLI overrides separately. The effective value is resolved with the correct precedence (CLI > file > default), and both reads and resolved-config dumps use that layered value instead of the raw stored entry. --- include/xtrpg/config/ConfigManager.hpp | 51 ++++++++++++++++++++------ src/config/ConfigManager.cpp | 28 +++++++------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/include/xtrpg/config/ConfigManager.hpp b/include/xtrpg/config/ConfigManager.hpp index 59933fa..e03e141 100644 --- a/include/xtrpg/config/ConfigManager.hpp +++ b/include/xtrpg/config/ConfigManager.hpp @@ -26,6 +26,31 @@ namespace xtrpg::config { */ using ConfigValue = std::variant; +/** + * Represents a configuration value with tiered overrides, allowing for + * system defaults, file-based overrides, and command-line argument overrides. + */ +struct TieredConfigValue { + /** The default value for the configuration option. */ + ConfigValue systemDefault; + + /** The value overridden by a configuration file. */ + std::optional fileOverride{std::nullopt}; + + /** The value overridden by a command-line argument. */ + std::optional cliOverride{std::nullopt}; + + /** + * Retrieves the effective value for the configuration option, considering + * the hierarchy of overrides: CLI > File > System Default. + * + * @returns The effective ConfigValue based on the override hierarchy. + */ + ConfigValue getEffectiveValue() const { + return cliOverride.value_or(fileOverride.value_or(systemDefault)); + } +}; + /** * Represents a single configuration option with its metadata. */ @@ -59,7 +84,8 @@ struct ModuleConfig { std::string name; /** - * Optional description of the module, used for help output and documentation. + * Optional description of the module, used for help output and + * documentation. */ std::string description; @@ -100,7 +126,8 @@ class ConfigManager { /** * Stores the resolved configuration values, organized by section and key. */ - std::unordered_map> + std::unordered_map> m_values; /** @@ -145,9 +172,9 @@ class ConfigManager { std::function()>; /** - * Returns a reference to the static registry of module configuration provider - * factories, allowing modules to register their configuration schemas at - * compile time. + * Returns a reference to the static registry of module configuration + * provider factories, allowing modules to register their configuration + * schemas at compile time. */ static std::vector &getRegistry() { static std::vector registry; @@ -155,8 +182,8 @@ class ConfigManager { } /** - * Registers a module's configuration provider factory in the static registry, - * allowing it to be discovered and included in the final resolved + * Registers a module's configuration provider factory in the static + * registry, allowing it to be discovered and included in the final resolved * configuration values. * * @param factory A factory function that returns a unique_ptr to an @@ -180,8 +207,8 @@ class ConfigManager { } /** - * Registers a module's configuration schema with the ConfigManager, allowing - * it to be included in the final resolved configuration values. + * Registers a module's configuration schema with the ConfigManager, + * allowing it to be included in the final resolved configuration values. */ void registerModule(const IModuleConfigProvider &provider); @@ -208,8 +235,8 @@ class ConfigManager { /** * Retrieves the resolved configuration value for a given section and key, - * returning it as the specified type T. Throws an exception if the section or - * key does not exist or if the type does not match. + * returning it as the specified type T. Throws an exception if the section + * or key does not exist or if the type does not match. */ template std::optional get(const std::string §ion, @@ -224,7 +251,7 @@ class ConfigManager { return std::nullopt; } - return std::get(keyIt->second); + return std::get(keyIt->second.getEffectiveValue()); } /** diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 64e546c..febc832 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -8,13 +8,10 @@ namespace xtrpg::config { */ void ConfigManager::registerModule(const IModuleConfigProvider &provider) { ModuleConfig schema = provider.getConfigSchema(); - std::string section = schema.name; - - m_schemas[section] = schema.options; - + m_schemas[schema.name] = schema.options; for (const auto &opt : schema.options) { // Apply lowest priority layers first: Module / Platform Defaults - m_values[section][opt.key] = opt.defaultValue; + m_values[schema.name][opt.key] = {.systemDefault = opt.defaultValue}; } } @@ -73,19 +70,20 @@ bool ConfigManager::loadTomlFile(const std::string &fileContent) { // Deduce type & assign (Overwrites module/compile defaults) if (valStr == "true") // Boolean type set to true - m_values[currentSection][key] = true; + m_values[currentSection][key].fileOverride = true; else if (valStr == "false") // Boolean type set to false - m_values[currentSection][key] = false; + m_values[currentSection][key].fileOverride = false; else if (valStr.front() == '"' && valStr.back() == '"') { // String type - m_values[currentSection][key] = valStr.substr(1, valStr.size() - 2); + m_values[currentSection][key].fileOverride = + valStr.substr(1, valStr.size() - 2); } else if (valStr.find('.') != std::string::npos) { // Double type - m_values[currentSection][key] = std::stod(valStr); + m_values[currentSection][key].fileOverride = std::stod(valStr); } else { // Integer type - m_values[currentSection][key] = std::stoll(valStr); + m_values[currentSection][key].fileOverride = std::stoll(valStr); } } } @@ -132,13 +130,13 @@ void ConfigManager::parseCLI(int argc, char *argv[]) { if (!valueStr.empty()) { if (valueStr == "true") - m_values[section][key] = true; + m_values[section][key].cliOverride = true; else if (valueStr == "false") - m_values[section][key] = false; + m_values[section][key].cliOverride = false; else if (std::all_of(valueStr.begin(), valueStr.end(), ::isdigit)) { - m_values[section][key] = std::stoll(valueStr); + m_values[section][key].cliOverride = std::stoll(valueStr); } else { - m_values[section][key] = valueStr; + m_values[section][key].cliOverride = valueStr; } } } @@ -171,7 +169,7 @@ void ConfigManager::dumpResolvedConfig(std::ostream &os) const { } // output the key and its corresponding value in a readable format - os << key << " = " << formatValue(value) << "\n"; + os << key << " = " << formatValue(value.getEffectiveValue()) << "\n"; } os << "\n"; } From 7cef39f06ec0a452273188e99ab97ecf221c0ee3 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 15:24:36 +1000 Subject: [PATCH 06/11] Load config.toml before CLI parsing The app now opens ./config.toml and feeds it into ConfigManager before parsing command-line arguments. ConfigManager's TOML loader was updated to accept an input stream instead of a string, allowing file-based configuration loading without copying the entire file into memory. --- apps/main.cpp | 8 +++++++- include/xtrpg/config/ConfigManager.hpp | 5 +++-- src/config/ConfigManager.cpp | 5 ++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/main.cpp b/apps/main.cpp index 404571c..2fe2a9e 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -1,12 +1,18 @@ +#include #include #include "xtrpg/config/ConfigManager.hpp" int main(int argc, char *argv[]) { - // Discover and register all modules' configuration schemas + // Discover and register all modules' configuration schemas, then load the + // configuration file and parse command-line arguments. xtrpg::config::ConfigManager configManager; configManager.registerAllDiscoveredModules(); + std::ifstream configFile("./config.toml"); + if (configFile) { + configManager.loadTomlFile(configFile); + } configManager.parseCLI(argc, argv); return 0; diff --git a/include/xtrpg/config/ConfigManager.hpp b/include/xtrpg/config/ConfigManager.hpp index e03e141..bd270c2 100644 --- a/include/xtrpg/config/ConfigManager.hpp +++ b/include/xtrpg/config/ConfigManager.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -224,8 +223,10 @@ class ConfigManager { /** * Loads configuration values from a TOML file, parsing the content and * applying the values to the appropriate sections and keys. + * + * @param input The stream containing the TOML configuration. */ - bool loadTomlFile(const std::string &fileContent); + bool loadTomlFile(std::istream &input); /** * Parses command-line arguments, updating configuration values based on the diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index febc832..4e78c33 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -38,12 +38,11 @@ void ConfigManager::printHelp(std::ostream &os) const { * Loads configuration values from a TOML file, parsing the content and * applying the values to the appropriate sections and keys. */ -bool ConfigManager::loadTomlFile(const std::string &fileContent) { - std::stringstream ss(fileContent); +bool ConfigManager::loadTomlFile(std::istream &input) { std::string line; std::string currentSection = "global"; - while (std::getline(ss, line)) { + while (std::getline(input, line)) { // Trim simple whitespace line.erase(0, line.find_first_not_of(" \t\r\n")); line.erase(line.find_last_not_of(" \t\r\n") + 1); From 153e27ecf30eddea1ae2edb4c9369166c8d00ed2 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 15:52:51 +1000 Subject: [PATCH 07/11] Remove redundant comment in ConfigManager.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove an unnecessary comment in ConfigManager.cpp that described applying lowest-priority configuration layers. This change has no functional impact — it simply cleans up the source by deleting an outdated/comment-only line. --- src/config/ConfigManager.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 4e78c33..50f2e61 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -10,7 +10,6 @@ void ConfigManager::registerModule(const IModuleConfigProvider &provider) { ModuleConfig schema = provider.getConfigSchema(); m_schemas[schema.name] = schema.options; for (const auto &opt : schema.options) { - // Apply lowest priority layers first: Module / Platform Defaults m_values[schema.name][opt.key] = {.systemDefault = opt.defaultValue}; } } From 4b5e84c25b3520f76470b1e0420505165f082d32 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 15:53:16 +1000 Subject: [PATCH 08/11] Add startup banner to server Adds a brief startup banner in apps/main.cpp before configuration loading, including application metadata, licensing details, and a warranty notice to identify the server and its open-source terms. --- apps/main.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/main.cpp b/apps/main.cpp index 2fe2a9e..fc7a888 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -5,6 +5,21 @@ int main(int argc, char *argv[]) { + // Output preamble information about the application and its configuration + // system. + std::cout << "XTRPG C++ Server" << std::endl + << "Version: 0.1.0" << std::endl + << "Copyright (C) 2026 XTRPG Contributors" << std::endl + << "License: MIT" << std::endl + << " " + "" + << std::endl + << std::endl + << "This is free and open-source software. You are free to use, " + "modify, and redistribute it under the terms of the MIT License." + << std::endl + << "There is NO WARRANTY for this software." << std::endl; + // Discover and register all modules' configuration schemas, then load the // configuration file and parse command-line arguments. xtrpg::config::ConfigManager configManager; From 815fedb8d2da1c16216a0c4d043f35305958d9da Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 15:54:16 +1000 Subject: [PATCH 09/11] Update app banner to XMPP branding This commit updates the startup banner in the main application entry point to reflect the project's XMPP server branding. It changes the displayed title from "XTRPG C++ Server" to "XTRPG: A XMPP Server" while keeping the rest of the version and licensing metadata unchanged. --- apps/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/main.cpp b/apps/main.cpp index fc7a888..97dca81 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -7,7 +7,7 @@ int main(int argc, char *argv[]) { // Output preamble information about the application and its configuration // system. - std::cout << "XTRPG C++ Server" << std::endl + std::cout << "XTRPG: A XMPP Server" << std::endl << "Version: 0.1.0" << std::endl << "Copyright (C) 2026 XTRPG Contributors" << std::endl << "License: MIT" << std::endl From 3195e8aed3320236a2db75794e296b45558da2d0 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 16:01:13 +1000 Subject: [PATCH 10/11] Improve CLI help output Add a blank line after the software banner and improve generated CLI help output by including an explicit Options section and a --help/-h entry. This makes the command-line interface easier to read and more user-friendly. --- apps/main.cpp | 3 ++- src/config/ConfigManager.cpp | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/main.cpp b/apps/main.cpp index 97dca81..2ecab76 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -18,7 +18,8 @@ int main(int argc, char *argv[]) { << "This is free and open-source software. You are free to use, " "modify, and redistribute it under the terms of the MIT License." << std::endl - << "There is NO WARRANTY for this software." << std::endl; + << "There is NO WARRANTY for this software." << std::endl + << std::endl; // Discover and register all modules' configuration schemas, then load the // configuration file and parse command-line arguments. diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 50f2e61..670cc18 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -21,15 +21,17 @@ void ConfigManager::registerModule(const IModuleConfigProvider &provider) { * @param os The output stream to which the help menu will be printed. */ void ConfigManager::printHelp(std::ostream &os) const { - os << "Usage: app [options]\n\n"; + os << "Usage: app [options]" << std::endl << std::endl; + os << "Options:" << std::endl; + os << " --help, -h Show this help message and exit" << std::endl; for (const auto &[section, options] : m_schemas) { - os << "[" << section << "]\n"; + os << "[" << section << "]" << std::endl; for (const auto &opt : options) { - os << "--" << section << "." << opt.key; - os << "\n " << opt.description; - os << " (Default: " << formatValue(opt.defaultValue) << ")\n"; + os << "--" << section << "." << opt.key << std::endl + << " " << opt.description + << " (Default: " << formatValue(opt.defaultValue) << ")\n"; } - os << "\n"; + os << std::endl; } } From f3140e19f8dde4b9704f98b6e2f6a0c855f6d350 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Fri, 21 Aug 2026 16:02:46 +1000 Subject: [PATCH 11/11] Use std::endl in dumpResolvedConfig Replace occurrences of '\n' with std::endl in src/config/ConfigManager.cpp::dumpResolvedConfig so section headers, option descriptions, key/value lines, and blank lines are written with std::endl (ensures consistent line termination and flush behavior). --- src/config/ConfigManager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 670cc18..9cb86fa 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -153,7 +153,7 @@ void ConfigManager::parseCLI(int argc, char *argv[]) { void ConfigManager::dumpResolvedConfig(std::ostream &os) const { for (const auto &[section, options] : m_values) { - os << "[" << section << "]\n"; + os << "[" << section << "]" << std::endl; for (const auto &[key, value] : options) { // output the key's documentation/description if available @@ -164,14 +164,14 @@ void ConfigManager::dumpResolvedConfig(std::ostream &os) const { schemaOptions.begin(), schemaOptions.end(), [&key](const ConfigOption &opt) { return opt.key == key; }); if (optIt != schemaOptions.end()) { - os << "# " << optIt->description << "\n"; + os << "# " << optIt->description << std::endl; } } // output the key and its corresponding value in a readable format - os << key << " = " << formatValue(value.getEffectiveValue()) << "\n"; + os << key << " = " << formatValue(value.getEffectiveValue()) << std::endl; } - os << "\n"; + os << std::endl; } }