diff --git a/Makefiles/Makefile.SDL.Win32.GCC b/Makefiles/Makefile.SDL.Win32.GCC index d35af16..bed71ae 100644 --- a/Makefiles/Makefile.SDL.Win32.GCC +++ b/Makefiles/Makefile.SDL.Win32.GCC @@ -119,7 +119,8 @@ endif # # Objects and Dependencies # -OBJ = $(OBJ_DIR)/PPCDisasm.o $(OBJ_DIR)/Games.o $(OBJ_DIR)/Config.o $(OBJ_DIR)/INIFile.o $(OBJ_DIR)/BlockFile.o $(OBJ_DIR)/93C46.o \ +OBJ = $(OBJ_DIR)/Format.o $(OBJ_DIR)/NewConfig.o \ + $(OBJ_DIR)/PPCDisasm.o $(OBJ_DIR)/Games.o $(OBJ_DIR)/Config.o $(OBJ_DIR)/INIFile.o $(OBJ_DIR)/BlockFile.o $(OBJ_DIR)/93C46.o \ $(OBJ_DIR)/ROMLoad.o $(OBJ_DIR)/unzip.o $(OBJ_DIR)/ioapi.o $(OBJ_DIR)/Error.o $(OBJ_DIR)/glew.o $(OBJ_DIR)/Shader.o \ $(OBJ_DIR)/Real3D.o $(OBJ_DIR)/Legacy3D.o $(OBJ_DIR)/Models.o $(OBJ_DIR)/TextureRefs.o \ $(OBJ_DIR)/New3D.o $(OBJ_DIR)/Mat4.o $(OBJ_DIR)/Model.o $(OBJ_DIR)/PolyHeader.o $(OBJ_DIR)/Texture.o $(OBJ_DIR)/TextureSheet.o $(OBJ_DIR)/VBO.o $(OBJ_DIR)/Vec.o $(OBJ_DIR)/R3DShader.o $(OBJ_DIR)/R3DFloat.o \ @@ -135,7 +136,7 @@ OBJ = $(OBJ_DIR)/PPCDisasm.o $(OBJ_DIR)/Games.o $(OBJ_DIR)/Config.o $(OBJ_DIR)/I $(OBJ_DIR)/amp_layer2.o $(OBJ_DIR)/amp_layer3.o $(OBJ_DIR)/amp_misc2.o $(OBJ_DIR)/amp_position.o $(OBJ_DIR)/amp_transform.o \ $(OBJ_DIR)/amp_util.o \ $(OBJ_DIR)/Crypto.o \ - $(OBJ_DIR)/Format.o + $(OBJ_DIR)/Logger.o # If built-in debugger enabled, include all debugging classes @@ -157,6 +158,10 @@ ppcd: $(BIN_DIR) $(OBJ_DIR) $(CXX) Src/CPU/PowerPC/PPCDisasm.cpp $(CPPFLAGS) -DSTANDALONE -o $(OBJ_DIR)/ppcd.o $(LD) -o $(BIN_DIR)/ppcd.exe -mconsole $(OBJ_DIR)/ppcd.o +tests: $(BIN_DIR) $(OBJ_DIR) $(OBJ) Src/Util/Test_Config.cpp + $(CXX) Src/Util/Test_Config.cpp $(CPPFLAGS) -o $(OBJ_DIR)/Test_Config.o + $(LD) -o $(BIN_DIR)/test_config.exe -mconsole $(OBJ_DIR)/Test_Config.o $(OBJ_DIR)/Format.o $(OBJ_DIR)/NewConfig.o $(OBJ_DIR)/Logger.o + $(BIN_DIR): mkdir $(BIN_DIR) diff --git a/Src/OSD/Logger.cpp b/Src/OSD/Logger.cpp new file mode 100644 index 0000000..92b91ac --- /dev/null +++ b/Src/OSD/Logger.cpp @@ -0,0 +1,45 @@ +#include "OSD/Logger.h" + +// Logger object is used to redirect log messages appropriately +static CLogger *s_Logger = NULL; + +CLogger *GetLogger() +{ + return s_Logger; +} + +void SetLogger(CLogger *Logger) +{ + s_Logger = Logger; +} + +void DebugLog(const char *fmt, ...) +{ + if (s_Logger == NULL) + return; + va_list vl; + va_start(vl, fmt); + s_Logger->DebugLog(fmt, vl); + va_end(vl); +} + +void InfoLog(const char *fmt, ...) +{ + if (s_Logger == NULL) + return; + va_list vl; + va_start(vl, fmt); + s_Logger->InfoLog(fmt, vl); + va_end(vl); +} + +bool ErrorLog(const char *fmt, ...) +{ + if (s_Logger == NULL) + return FAIL; + va_list vl; + va_start(vl, fmt); + s_Logger->ErrorLog(fmt, vl); + va_end(vl); + return FAIL; +} diff --git a/Src/OSD/Logger.h b/Src/OSD/Logger.h index c750b81..29586fc 100644 --- a/Src/OSD/Logger.h +++ b/Src/OSD/Logger.h @@ -239,8 +239,8 @@ private: /****************************************************************************** Log Functions - Message logging interface. All messages are passed by the OSD layer to the - currently active logger object. + Message logging interface. All messages are passed to the currently active + logger object. ******************************************************************************/ /* diff --git a/Src/OSD/SDL/Main.cpp b/Src/OSD/SDL/Main.cpp index 9336179..edb1a43 100644 --- a/Src/OSD/SDL/Main.cpp +++ b/Src/OSD/SDL/Main.cpp @@ -63,59 +63,10 @@ #include "WinOutputs.h" #endif - -/****************************************************************************** - Error and Debug Logging -******************************************************************************/ - // Log file names #define DEBUG_LOG_FILE "debug.log" #define ERROR_LOG_FILE "error.log" -// Logger object is used to redirect log messages appropriately -static CLogger *s_Logger = NULL; - -CLogger *GetLogger() -{ - return s_Logger; -} - -void SetLogger(CLogger *Logger) -{ - s_Logger = Logger; -} - -void DebugLog(const char *fmt, ...) -{ - if (s_Logger == NULL) - return; - va_list vl; - va_start(vl, fmt); - s_Logger->DebugLog(fmt, vl); - va_end(vl); -} - -void InfoLog(const char *fmt, ...) -{ - if (s_Logger == NULL) - return; - va_list vl; - va_start(vl, fmt); - s_Logger->InfoLog(fmt, vl); - va_end(vl); -} - -bool ErrorLog(const char *fmt, ...) -{ - if (s_Logger == NULL) - return FAIL; - va_list vl; - va_start(vl, fmt); - s_Logger->ErrorLog(fmt, vl); - va_end(vl); - return FAIL; -} - /****************************************************************************** Display Management @@ -1549,9 +1500,9 @@ static void PrintGameList(void) * * Program entry point. */ + int main(int argc, char **argv) { - #ifdef SUPERMODEL_DEBUGGER bool cmdEnterDebugger = false; #endif // SUPERMODEL_DEBUGGER diff --git a/Src/Util/Format.cpp b/Src/Util/Format.cpp index 941d40e..f7114d8 100644 --- a/Src/Util/Format.cpp +++ b/Src/Util/Format.cpp @@ -1,10 +1,33 @@ #include "Util/Format.h" +#include namespace Util { + std::string TrimWhiteSpace(const std::string &str) + { + if (str.empty()) + return std::string(); + size_t first = 0; + for (; first < str.length(); ++first) + { + if (!isspace(str[first])) + break; + } + if (first >= str.length()) + return std::string(); + size_t last = str.length() - 1; + for (; last > first; --last) + { + if (!isspace(str[last])) + break; + } + ++last; + return std::string(str.c_str() + first, last - first); + } + static const char hex_digits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; - const std::string Hex(uint32_t n, size_t num_digits) + std::string Hex(uint32_t n, size_t num_digits) { Util::Format f; f << "0x"; @@ -16,17 +39,17 @@ namespace Util return f; } - const std::string Hex(uint32_t n) + std::string Hex(uint32_t n) { return Hex(n, 8); } - const std::string Hex(uint16_t n) + std::string Hex(uint16_t n) { return Hex(n, 4); } - const std::string Hex(uint8_t n) + std::string Hex(uint8_t n) { return Hex(n, 2); } diff --git a/Src/Util/Format.h b/Src/Util/Format.h index 3fdea00..f10d02d 100644 --- a/Src/Util/Format.h +++ b/Src/Util/Format.h @@ -84,10 +84,11 @@ namespace Util } }; - const std::string Hex(uint32_t n, size_t num_digits); - const std::string Hex(uint32_t n); - const std::string Hex(uint16_t n); - const std::string Hex(uint8_t n); + std::string TrimWhiteSpace(const std::string &str); + std::string Hex(uint32_t n, size_t num_digits); + std::string Hex(uint32_t n); + std::string Hex(uint16_t n); + std::string Hex(uint8_t n); } // Util #endif // INCLUDED_FORMAT_H diff --git a/Src/Util/NewConfig.cpp b/Src/Util/NewConfig.cpp new file mode 100644 index 0000000..3e3b515 --- /dev/null +++ b/Src/Util/NewConfig.cpp @@ -0,0 +1,372 @@ +/* + * Config Tree + * ----------- + * + * A hierarchical data structure supporting arbitrary nesting. Each node + * (Config::Node) has a key and either a value or children (in fact, it may + * have both, but this does not make semantic sense and so the config tree + * builders take care not to allow it). + * + * All values are simply strings for now. They may have defaults, which are + * actually supplied by the caller and returned when a queried node cannot be + * found. + * + * Nodes at the same nesting level (siblings) are strung together in a + * linked list. Parents also maintain pointers to the first and last of their + * children (for order-preserving iteration) as well as a map for direct + * lookup by key. + * + * Keys may be reused at a given level. Key-value pairs will have their order + * preserved when iterated but only the most recent key is returned in direct + * map lookups. + * + * Example: + * + * + * + * + * + * + * + * config["game/roms"].Value() <-- should return nothing + * config["game/roms/crom/offset"].Value() <-- "2", the second tag + * config["game/roms"].begin() <-- iterate over two tags + * + * INI Semantics + * ------------- + * + * INI files are linear in nature and divided into sections. Config trees built + * to represent INI files must adhere to the following rules: + * + * - Section nodes have their key set to the section name and value empty. + * They are the only nodes that can have children (i.e., IsLeaf() == false, + * HasChildren() == true). + * - Top-level node in the tree is the global section, and its key is + * "Global". + * - Only the global section (top-level section) may have child nodes that are + * sections. The config tree can therefore only be up to 2 levels deep: the + * first, top-most level consists of global parameters, and section nodes + * provide one more level of nesting. + * + * INI files contain zero or more sections followed by zero or more settings + * each. + * + * Setting0 = 100 + * [ Global ] + * Setting1 = 200 + * [ Section1 ] + * SettingA = 1 ; this is a comment + * SettingB = foo + * [ Section2 ] + * SettingX = bar + * SettingZ = "baz" ; quotes are optional and are stripped off during parsing + * + * Any setting not explicitly part of a section is assigned to the "Global" + * section. For example, Setting0 and Setting1 above are both part of "Global". + */ + +#include "Util/NewConfig.h" +#include "OSD/Logger.h" +#include +#include +#include + +namespace Util +{ + namespace Config + { + Node Node::s_empty_node; + + const Node &Node::operator[](const std::string &path) const + { + const Node *e = this; + std::vector keys = Util::Format(path).Split('/'); + for (auto &key: keys) + { + auto it = e->m_children.find(key); + if (it == e->m_children.end()) + return s_empty_node; + e = it->second.get(); + } + return *e; + } + + Node &Node::Get(const std::string &path) + { + return const_cast(operator[](path)); + } + + const Node &Node::Get(const std::string &path) const + { + return operator[](path); + } + + void Node::Print(size_t indent_level) const + { + std::fill_n(std::ostream_iterator(std::cout), 2 * indent_level, ' '); + std::cout << m_key; + if (m_value.length()) + std::cout << '=' << m_value; + std::cout << " children={"; + for (auto v: m_children) + std::cout << ' ' << v.first; + std::cout << " }" << std::endl; + for (Ptr_t child = m_first_child; child; child = child->m_next_sibling) + child->Print(indent_level + 1); + } + + Node &Node::Create(const std::string &key) + { + Ptr_t node = std::make_shared(key); + AddChild(node); + return *node; + } + + Node &Node::Create(const std::string &key, const std::string &value) + { + Ptr_t node = std::make_shared(key, value); + AddChild(node); + return *node; + } + + // Adds a newly-created node (which, among other things, implies no + // children) as a child + void Node::AddChild(Ptr_t &node) + { + if (!m_last_child) + { + m_first_child = node; + m_last_child = node; + } + else + { + m_last_child->m_next_sibling = node; + m_last_child = node; + } + m_children[node->m_key] = node; + } + + /* + void Node::Clear() + { + m_next_sibling.reset(); + m_first_child.reset(); + m_last_child.reset(); + m_children.clear(); + m_key.clear(); + m_value.clear(); + } + */ + + Node::Node() + { + //std::cout << "Created " << "" << " (" << this << ")" << std::endl; + } + + Node::Node(const std::string &key) + : m_key(key) + { + //std::cout << "Created " << key << " (" << this << ")" << std::endl; + } + + Node::Node(const std::string &key, const std::string &value) + : m_key(key), + m_value(value) + { + //std::cout << "Created " << key << '=' << value << " (" << this << ")" << std::endl; + } + + // Deep copy + Node::Node(const Node &that) + : m_key(that.m_key), + m_value(that.m_value) + { + for (Ptr_t child = that.m_first_child; child; child = child->m_next_sibling) + { + Ptr_t copied_child = std::make_shared(*child); + AddChild(copied_child); + } + } + + Node::~Node() + { + //std::cout << "Destroyed " << m_key << " (" << this << ")" << std::endl; + } + + Node::Ptr_t CreateEmpty() + { + return std::shared_ptr(new Node()); + } + + static std::string StripComment(const std::string &line) + { + // Find first semicolon not enclosed in "" + bool inside_quotes = false; + for (auto it = line.begin(); it != line.end(); ++it) + { + inside_quotes ^= (*it == '\"'); + if (!inside_quotes && *it == ';') + return std::string(line.begin(), it); + } + return line; + } + + static void ParseAssignment(Node ¤t_section, const std::string &filename, size_t line_num, const std::string &line) + { + size_t idx_equals = line.find_first_of('='); + if (idx_equals == std::string::npos) + { + //std::cerr << filename << ':' << line_num << ": Syntax error. No '=' found." << std::endl; + ErrorLog("%s:%d: Syntax error. No '=' found.", filename.c_str(), line_num); + return; + } + std::string lvalue(TrimWhiteSpace(std::string(line.begin(), line.begin() + idx_equals))); + if (lvalue.empty()) + { + //std::cerr << filename << ':' << line_num << ": Syntax error. Setting name missing before '='." << std::endl; + ErrorLog("%s:%d: Syntax error. Setting name missing before '='.", filename.c_str(), line_num); + return; + } + // Get rvalue, which is allowed to be empty and strip off quotes if they + // exist. Only a single pair of quotes encasing the rvalue are permitted. + std::string rvalue(TrimWhiteSpace(std::string(line.begin() + idx_equals + 1, line.end()))); + size_t idx_first_quote = rvalue.find_first_of('\"'); + size_t idx_last_quote = rvalue.find_last_of('\"'); + if (idx_first_quote == 0 && idx_last_quote == (rvalue.length() - 1) && idx_first_quote != idx_last_quote) + rvalue = std::string(rvalue.begin() + idx_first_quote + 1, rvalue.begin() + idx_last_quote); + if (std::count(rvalue.begin(), rvalue.end(), '\"') != 0) + { + //std::cerr << filename << ':' << line_num << ": Warning: Extraneous quotes present on line." << std::endl; + ErrorLog("%s:%d: Warning: Extraneous quotes present on line.", filename.c_str(), line_num); + } + // In INI files, we do not allow multiple settings with the same key. If + // a setting is specified multiple times, previous ones are overwritten. + if (current_section[lvalue].Empty()) + current_section.Create(lvalue, rvalue); + else + current_section.Get(lvalue).SetValue(rvalue); + } + + Node::Ptr_t FromINIFile(const std::string &filename) + { + std::ifstream file; + file.open(filename); + if (file.fail()) + { + ErrorLog("Unable to open '%s'. Configuration will not be loaded.", filename.c_str()); + return CreateEmpty(); + } + + Node::Ptr_t global_ptr = std::make_shared("Global"); // the root is also the "Global" section + Node &global = *global_ptr; + Node *current_section = &global; + + size_t line_num = 1; + while (!file.eof()) + { + if (file.fail()) + { + ErrorLog("%s:%d: File read error. Configuration will be incomplete."); + return CreateEmpty(); + } + + std::string line; + std::getline(file, line); + line = StripComment(line); + line = TrimWhiteSpace(line); + + if (!line.empty()) + { + if (*line.begin() == '[' && *line.rbegin() == ']') + { + // Check if section exists else create a new one + std::string section(TrimWhiteSpace(std::string(line.begin() + 1, line.begin() + line.length() - 1))); + if (section.empty() || section == "Global") // empty section names (e.g., "[]") assigned to "[ Global ]" + current_section = &global; + else if (global[section].Empty()) + { + Node &new_section = global.Create(section); + current_section = &new_section; + } + else + current_section = &global.Get(section); + } + else + { + ParseAssignment(*current_section, filename, line_num, line); + } + } + + line_num++; + } + + return global_ptr; + } + + /* + * Produces a new INI section by merging two existing sections. + * + * - Nodes from x and y with children are ignored as per INI semantics. + * The presence of children indicates a section, not a setting. + * - Settings from y override settings in x if already present. + * - x's key is retained. + */ + Node::Ptr_t MergeINISections(const Node &x, const Node &y) + { + Node::Ptr_t merged_ptr = std::make_shared(x.Key()); // result has same key as x + Node &merged = *merged_ptr; + // First copy settings from section x + for (auto it = x.begin(); it != x.end(); ++it) + { + if (it->IsLeaf()) + merged.Create(it->Key(), it->Value()); + } + // Merge in settings from section y + for (auto it = y.begin(); it != y.end(); ++it) + { + auto &key = it->Key(); + auto &value = it->Value(); + if (it->IsLeaf()) + { + if (merged[key].Empty()) + merged.Create(key, value); + else + merged.Get(key).SetValue(value); + } + } + return merged_ptr; + } + + static void WriteSection(std::ofstream &file, const Node §ion) + { + file << "[ " << section.Key() << " ]" << std::endl; + file << std::endl; + for (auto it = section.begin(); it != section.end(); ++it) + { + if (it->IsLeaf()) + file << it->Key() << " = \"" << it->Value() << "\"" << std::endl; + } + file << std::endl << std::endl; + } + + void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment) + { + std::ofstream file; + file.open(filename); + if (file.fail()) + { + ErrorLog("Unable to write to '%s'. Configuration will not be saved."); + return; + } + if (!header_comment.empty()) + file << header_comment << std::endl << std::endl; + WriteSection(file, config); + for (auto it = config.begin(); it != config.end(); ++it) + { + if (it->HasChildren()) + WriteSection(file, *it); + } + } + } // Config +} // Util + diff --git a/Src/Util/NewConfig.h b/Src/Util/NewConfig.h new file mode 100644 index 0000000..fe401ff --- /dev/null +++ b/Src/Util/NewConfig.h @@ -0,0 +1,204 @@ +#ifndef INCLUDED_UTIL_CONFIG_H +#define INCLUDED_UTIL_CONFIG_H + +#include "Util/Format.h" +#include +#include +#include + +namespace Util +{ + namespace Config + { + class Node + { + public: + typedef std::shared_ptr Ptr_t; + typedef std::shared_ptr ConstPtr_t; + + private: + Ptr_t m_next_sibling; + Ptr_t m_first_child; + Ptr_t m_last_child; + std::map m_children; + const std::string m_key; + std::string m_value; + static Node s_empty_node; // key, value, and children must always be empty + + void AddChild(Ptr_t &node); + Node(); // prohibit accidental/unintentional creation of blank nodes + friend Ptr_t CreateEmpty(); + + public: + class const_iterator; + + class iterator: public std::iterator + { + private: + Ptr_t m_node; + friend class const_iterator; + public: + inline iterator(Ptr_t node = Ptr_t()) + : m_node(node) + {} + inline iterator(const iterator &it) + : m_node(it.m_node) + {} + inline iterator operator++() + { + // Prefix increment + m_node = m_node->m_next_sibling; + return *this; + } + inline iterator operator++(int) + { + // Postfix increment + iterator current(*this); + m_node = m_node->m_next_sibling; + return *this; + } + inline Node &operator*() const + { + return *m_node; + } + inline Ptr_t operator->() const + { + return m_node; + } + inline bool operator==(const iterator &rhs) const + { + return m_node == rhs.m_node; + } + inline bool operator!=(const iterator &rhs) const + { + return m_node != rhs.m_node; + } + }; + + class const_iterator: public std::iterator + { + private: + ConstPtr_t m_node; + public: + inline const_iterator(ConstPtr_t node = ConstPtr_t()) + : m_node(node) + {} + inline const_iterator(Ptr_t node = Ptr_t()) + : m_node(std::const_pointer_cast(node)) + {} + inline const_iterator(const const_iterator &it) + : m_node(it.m_node) + {} + inline const_iterator(const Node::iterator &it) + : m_node(it.m_node) + {} + inline const_iterator operator++() + { + // Prefix increment + m_node = m_node->m_next_sibling; + return *this; + } + inline const_iterator operator++(int) + { + // Postfix increment + iterator current(*this); + m_node = m_node->m_next_sibling; + return *this; + } + inline const Node &operator*() const + { + return *m_node; + } + inline ConstPtr_t operator->() const + { + return m_node; + } + inline bool operator==(const const_iterator &rhs) const + { + return m_node == rhs.m_node; + } + inline bool operator!=(const const_iterator &rhs) const + { + return m_node != rhs.m_node; + } + }; + + inline iterator begin() + { + return iterator(m_first_child); + } + + inline iterator end() + { + return iterator(); + } + + inline const_iterator begin() const + { + return iterator(m_first_child); + } + + inline const_iterator end() const + { + return iterator(); + } + + const inline std::string &Key() const + { + return m_key; + } + + const inline std::string &ValueWithDefault(const std::string &default_value) const + { + if (this == &s_empty_node) + return default_value; + return m_value; + } + + const inline std::string &Value() const + { + //TODO: if empty node, throw exception? + return m_value; + } + + inline void SetValue(const std::string &value) + { + if (this != &s_empty_node) // not allowed to modify empty node + m_value = value; + } + + inline bool Empty() const + { + return m_key.empty(); + } + + inline bool IsLeaf() const + { + return m_children.empty(); + } + + inline bool HasChildren() const + { + return !IsLeaf(); + } + + Node &Get(const std::string &path); + const Node &operator[](const std::string &path) const; + const Node &Get(const std::string &path) const; + void Print(size_t indent_level = 0) const; + Node &Create(const std::string &key); + Node &Create(const std::string &key, const std::string &value); + Node(const std::string &key); + Node(const std::string &key, const std::string &value); + Node(const Node &that); + ~Node(); + }; + + Node::Ptr_t CreateEmpty(); + Node::Ptr_t FromINIFile(const std::string &filename); + Node::Ptr_t MergeINISections(const Node &x, const Node &y); + void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment); + } // Config +} // Util + +#endif // INCLUDED_UTIL_CONFIG_H diff --git a/Src/Util/Test_Config.cpp b/Src/Util/Test_Config.cpp new file mode 100644 index 0000000..0602571 --- /dev/null +++ b/Src/Util/Test_Config.cpp @@ -0,0 +1,61 @@ +#include "Util/NewConfig.h" +#include + +int main() +{ + /* + * Creates a config tree corresponding to the following: + * + * + * scud + * + * + * foo.bin + * 0 + * + * + * bar.bin + * 2 + * + * + * + */ + Util::Config::Node::Ptr_t root = std::make_shared("global"); + auto &game = root->Create("game"); + game.Create("name", "scud"); + auto &roms = game.Create("roms"); + auto &crom1 = roms.Create("crom"); + crom1.Create("name", "foo.bin"); + crom1.Create("offset", "0"); + auto &crom2 = roms.Create("crom"); + crom2.Create("name", "bar.bin"); + crom2.Create("offset", "2"); + + const char *expected_output = + "global children={ game } \n" + " game children={ name roms } \n" + " name=scud children={ } \n" + " roms children={ crom } \n" + " crom children={ name offset } \n" + " name=foo.bin children={ } \n" + " offset=0 children={ } \n" + " crom children={ name offset } \n" + " name=bar.bin children={ } \n" + " offset=2 children={ } \n"; + std::cout << "Expected output:" << std::endl << std::endl << expected_output << std::endl; + std::cout << "Actual config tree:" << std::endl << std::endl; + root->Print(); + std::cout << std::endl; + + // Expect second crom: bar.bin + auto &global = *root; + std::cout << "game/roms/crom/name=" << global["game/roms/crom/name"].Value() << " (expected: bar.bin)" << std::endl << std::endl; + + // Make a copy + std::cout << "Copy:" << std::endl << std::endl; + Util::Config::Node::Ptr_t copy = std::make_shared(*root); + copy->Print(); + std::cout << std::endl; + return 0; +} + \ No newline at end of file