/* +------------------------------------------------------+ ____/ \____ /| - Open source game framework licensed to freely use, | \ / / | copy, modify and sell without restriction | +--\ ^__^ /--+ | | | ~/ \~ | | - created for | | ~~~~~~~~~~~~ | +------------------------------------------------------+ | SPACE ~~~~~ | / | ~~~~~~~ BOX |/ +-------------*/ #pragma once /* Standard library */ #include #include #include #include /* SPACEBOX distributed libraries */ #include "json/json.hpp" /* SPACEBOX */ #include "filesystem.hpp" #include "Animation.hpp" #include "Log.hpp" #include "extension.hpp" class Configuration { private: Animation auto_refresher = Animation(std::bind(&Configuration::refresh, this)); fs::file_time_type config_modification_time = fs::file_time_type::clock::now(); std::vector files_to_refresh; /*! * @warning This JSON will be copied when the object is copied, and it is potentially a large copy because it can copy an arbitrary * amount of JSON data. Because a game object by definition has a single configuration, this can be optimized in the future so that * the JSON is stored in a member of the game object. Arguably it also doesn't make sense to be copying the data because the JSON is * expected to change during runtime. */ nlohmann::json config; /*! * Fill the config JSON with default values set by the framework. */ void set_defaults(); /*! * Pass a JSON object along with key names to get value at the specified hierarchy of keys. * * @warning This probably should not be called directly and is just used to provide a recursive call for variadic * access through Configuration::operator() * * @param hierarchy string that appends the key as a string to the end as the function runs recursively to represent the hierarchy * in case of an error * @param json a JSON element in the configuration * @param key first-level key * @param keys zero or more next level keys */ template const nlohmann::json& access(std::ostringstream& hierarchy, const nlohmann::json& json, const Key& key, const Keys&... keys) const { hierarchy << '"' << key << '"'; if constexpr (sizeof...(keys) > 0) { hierarchy << " > "; return access(hierarchy, json[key], keys...); } else { hierarchy << ")"; try { return json.at(key); } catch (const std::exception& exception) { std::ostringstream message; message << "Error accessing configuration at " << hierarchy.str() << ": " << exception.what(); throw std::runtime_error(message.str()); } } } public: /*! * Construct a Configuration object. The path argument is the location to look for a user configuration. If valid JSON is found, * it will be merged into the hard-coded default assignments, overwriting any existing key/value pairs. The path will be watched * for changes if auto refresh is turned on. */ Configuration(); /*! * Get a writable JSON object corresponding to a key in the configuration. * * The JSON object's value can be used directly if it can be implictly type-cast, and it can be assigned a new value. See * https://nlohmann.github.io/json/api/basic_json/ for further information on how to read and write the JSON object. * * @param key Top level key corresponding to a section of the SPACEBOX configuration JSON * @return writable nlohmann::JSON object reference */ nlohmann::json& operator[](const std::string& key); /*! * Get a read-only JSON object reference to the specified keys, each given as a separate argument. If no keys are given, * a reference to the entire configuration JSON object is returned. * * @return read-only JSON object reference */ const nlohmann::json& operator[](const std::string& key) const; /*! * @return Read-only JSON object reference to the full configuration */ const nlohmann::json& operator()() const; /*! * Get a read-only reference to the JSON object at the given hierarchy of keys. The key must exist in the configuration, otherwise an exception * will be thrown. To get a reference to JSON object for a new key, use sb::Configuration::operator[](const std::string) instead. * * Example, * * std::cout << _configuration() << std::endl; * std::cout << _configuration("recording", "enabled") << " " << _configuration("input") << " " << * _configuration("levels", 0, 3, 1) << std::endl; * * Prints, * * [full config...] * true {"any-key-ignore-commands":[],"default-unsuppress-delay":0.7,"ignore-repeat-keypress":true,\ * "suppress-any-key-on-mods":true, "system-any-key-ignore-commands":["fullscreen","screenshot","record","quit"]} 261.0 * * @param keys hierarchy of keys used to look up a specific JSON object in the config * @return read-only reference to JSON object specified by keys */ template const nlohmann::json& operator()(const Key&... keys) const { std::ostringstream hierarchy; hierarchy << "("; return access(hierarchy, config, keys...); } /*! * Merge new SPACEBOX configuration JSON with the JSON already in memory, overwriting any existing key/value pairs. * * @param incoming JSON object populated with SPACEBOX configuration settings */ void merge(const nlohmann::json& incoming); /*! * Merge new SPACEBOX configuration from a given path to a JSON file. * * @param path JSON file path with new configuration values */ void merge(const fs::path& path); /*! * @overload void Configuration::merge(const fs::path& path) */ void merge(const std::string& path); /*! * @overload void Configuration::merge(const fs::path& path) */ void merge(const char* path); /*! * Open the JSON file at a given path and return the contents as a JSON object. * * @param path Filesystem path to a JSON file * @return A nlohmann::json object containing the contents of the given file */ static nlohmann::json json_from_file(const fs::path& path); /*! * @overload Configuration::json_from_file(const fs::path&) */ static nlohmann::json json_from_file(const std::string& path); /*! * @overload Configuration::json_from_file(const fs::path&) */ static nlohmann::json json_from_file(const char* path); /*! * Enable auto refresh. Auto refresh watches the file at the given path for changes and loads them automatically at every interval * specified in the configuration under "configuration" -> "auto-refresh-interval". * * @param file_to_refresh path to a configuration JSON */ void enable_auto_refresh(const fs::path& file_to_refresh); /*! * Disable auto refresh. The file previously set with Configuration::enable_auto_refresh will no longer be watched for changes. */ void disable_auto_refresh(); /*! * Check if the user config file was modified and merge if so. */ void refresh(); /*! * Update the auto refresher with the given timestamp, which should be the timestamp passed to Game::update. * * @param timestamp seconds elapsed since the program started */ void update(float timestamp); }; /* Extend GLM so nlohmann::json can read and write glm::vec */ namespace glm { template void to_json(nlohmann::json& j, const vec& v) { if constexpr (dimensions == 2) { j = nlohmann::json{v.x, v.y}; } else if constexpr (dimensions == 3) { j = nlohmann::json{v.x, v.y, v.z}; } else if constexpr (dimensions == 4) { j = nlohmann::json{v.x, v.y, v.z, v.w}; } } /*! * Convert JSON value into GLM vertex of 2 - 4 dimensions. The resulting vertex will be the dimensions specified in the template * argument. If the JSON value is an array with the same number of dimensions, it is converted directly into the vertex. If the * JSON value is a scalar, the vertex is created with the dimensions all set to the scalar. If the JSON value is an array of smaller * dimension, the missing dimensions are set to 0. If the JSON value is an array of larger dimension, the extra dimensions are left * out of the vertex. * * @param j JSON object containing a scalar or array to be converted into a 2-4D GLM vertex * @param v reference to a vertex that will be set to the value contained in the JSON */ template void from_json(const nlohmann::json& j, vec& v) { for (std::size_t ii = 0; ii < dimensions; ii++) { if (j.is_array()) { if (j.size() < ii + 1) { v[ii] = 0; } else { j.at(ii).get_to(v[ii]); } } else { j.get_to(v[ii]); } } } } /* Extend std::filesystem so nlohmann::json can read and write std::filesystem::path */ #if defined(__UBUNTU18__) namespace std::experimental::filesystem #else namespace std::filesystem #endif { template void to_json(nlohmann::json& j, const path& p) { j = nlohmann::json{p}; } template void from_json(const nlohmann::json& j, path& p) { j.at(0).get_to(p); } } namespace std { /*! * Stream the entire JSON when the stream operator is called. * * @param out Output stream * @param configuration Configuration object being output to the stream * @return A reference to the input stream */ std::ostream& operator<<(std::ostream& out, const Configuration& configuration); } /* Add Configuration class to the sb namespace. This should be the default location, but Configuration is left in the global namespace * for backward compatibility. */ namespace sb { using ::Configuration; } #include "Delegate.hpp"