From 98ad5d145745348a446260e968bfa5a1c417cacd Mon Sep 17 00:00:00 2001 From: poikilos <7557867+poikilos@users.noreply.github.com> Date: Wed, 17 Feb 2021 18:55:39 -0500 Subject: [PATCH] Add a settings system and related tests... ...See changelog for more. --- Engine.cpp | 1 + Engine.h | 3 +- Utility.cpp | 65 ++++++-- Utility.h | 9 +- b3view.pro | 6 +- changelog.md | 8 + settings.cpp | 454 +++++++++++++++++++++++++++++++++++++++++++++++++++ settings.h | 62 +++++++ 8 files changed, 591 insertions(+), 17 deletions(-) create mode 100644 settings.cpp create mode 100644 settings.h diff --git a/Engine.cpp b/Engine.cpp index b853e39..ca3df40 100644 --- a/Engine.cpp +++ b/Engine.cpp @@ -279,6 +279,7 @@ s32 Engine::getNumberOfVertices() Engine::Engine() { + settings.set_int("max_recent", 10); // For monitoring single press: see // for (u32 i = 0; i < KEY_KEY_CODES_COUNT; ++i) diff --git a/Engine.h b/Engine.h index a7774b6..3af08dd 100644 --- a/Engine.h +++ b/Engine.h @@ -13,6 +13,7 @@ class View; #include "EventHandler.h" #include "extlib/CGUITTFont.h" #include +#include "settings.h" enum SceneItemID { SIID_LIGHT = 1, @@ -66,7 +67,7 @@ private: bool KeyIsDown[irr::KEY_KEY_CODES_COUNT]; irr::s32 keyState[irr::KEY_KEY_CODES_COUNT]; irr::s32 LMouseState, RMouseState; - + Settings settings; public: std::wstring m_LoadedMeshPath; std::wstring m_LoadedTexturePath; diff --git a/Utility.cpp b/Utility.cpp index 1612a83..c8eb1f5 100644 --- a/Utility.cpp +++ b/Utility.cpp @@ -17,6 +17,8 @@ using namespace irr::scene; using namespace irr::video; using namespace std; +const std::string Utility::WHITESPACE = " \n\r\t\f\v"; + void Utility::dumpVectorToConsole(const vector3df& vector) { debug() << "X: " << vector.X << " Y: " << vector.Y << " Z: " << vector.Z << endl; @@ -152,6 +154,36 @@ bool Utility::startsWith(const std::wstring& haystack, const std::wstring& needl return found; } +bool Utility::endsWith(const std::wstring& haystack, const std::wstring& needle) { + bool found = false; + if (haystack.length() >= needle.length()) { + if (haystack.substr(haystack.length()-needle.length())==needle) { + found = true; + } + } + return found; +} + +bool Utility::startsWith(const std::string& haystack, const std::string& needle) { + bool found = false; + if (haystack.length() >= needle.length()) { + if (haystack.substr(0, needle.length())==needle) { + found = true; + } + } + return found; +} + +bool Utility::endsWith(const std::string& haystack, const std::string& needle) { + bool found = false; + if (haystack.length() >= needle.length()) { + if (haystack.substr(haystack.length()-needle.length())==needle) { + found = true; + } + } + return found; +} + wstring Utility::replaceAll(const wstring &subject, const wstring &from, const wstring &to) { size_t i = 0; @@ -190,16 +222,6 @@ std::string Utility::replaceAll(const std::string &subject, const std::string &f return result; } -bool Utility::endsWith(const std::wstring& haystack, const std::wstring& needle) { - bool found = false; - if (haystack.length() >= needle.length()) { - if (haystack.substr(haystack.length()-needle.length())==needle) { - found = true; - } - } - return found; -} - bool Utility::startsWithAny(const std::wstring& haystack, const std::vector& needles, bool CI) { return getPrefix(haystack, needles, CI).length() > 0; } @@ -447,6 +469,23 @@ std::string Utility::toString(irr::f32 val) return std::to_string(val); } +std::string Utility::ltrim(const std::string& s) +{ + size_t start = s.find_first_not_of(Utility::WHITESPACE); + return (start == std::string::npos) ? "" : s.substr(start); +} + +std::string Utility::rtrim(const std::string& s) +{ + size_t end = s.find_last_not_of(Utility::WHITESPACE); + return (end == std::string::npos) ? "" : s.substr(0, end + 1); +} + +std::string Utility::trim(const std::string& s) +{ + return rtrim(ltrim(s)); +} + // don't do late instantiation (see header file) // template // bool Utility::equalsApprox(T f1, T f2) @@ -477,17 +516,17 @@ void TestUtility::testReplaceAll(const std::string &subject, const std::string & void TestUtility::assertEqual(const wstring& subject, const wstring& expectedResult) { if (subject != expectedResult) { - cerr << "The test expected \"" << Utility::toString(expectedResult) << "\" but got \"" << Utility::toString(subject) << std::endl; + cerr << "The test expected \"" << Utility::toString(expectedResult) << "\" but got \"" << Utility::toString(subject) << "\"" << std::endl; } assert(subject == expectedResult); } void TestUtility::assertEqual(const std::string subject, const std::string expectedResult) { if (subject != expectedResult) { - cerr << "The test expected \"" << expectedResult << "\" but got \"" << subject << std::endl; + cerr << "The test expected \"" << expectedResult << "\" but got \"" << subject << "\"" << std::endl; } assert(subject == expectedResult); } -static TestUtility testutility; +static TestUtility testutility; // Run tests (Creating the first instance runs the static constructor). diff --git a/Utility.h b/Utility.h index 331aca0..4e0bad0 100644 --- a/Utility.h +++ b/Utility.h @@ -9,6 +9,7 @@ class Utility { public: + static const std::string WHITESPACE; static void dumpVectorToConsole(const irr::core::vector3df& vector); static int getTextureCount(const irr::video::SMaterial& material); static int getTextureCount(irr::scene::IAnimatedMeshSceneNode* node); @@ -20,9 +21,11 @@ public: static std::wstring rightOf(const std::wstring& path, const std::wstring& delimiter, bool allIfNotFound); static std::wstring rightOfLast(const std::wstring& path, const std::wstring& delimiter, bool allIfNotFound); static bool startsWith(const std::wstring& haystack, const std::wstring& needle); + static bool endsWith(const std::wstring& haystack, const std::wstring& needle); + static bool startsWith(const std::string& haystack, const std::string& needle); + static bool endsWith(const std::string& haystack, const std::string& needle); static std::wstring replaceAll(const std::wstring& subject, const std::wstring& from, const std::wstring& to); static std::string replaceAll(const std::string& subject, const std::string& from, const std::string& to); - static bool endsWith(const std::wstring& haystack, const std::wstring& needle); static std::wstring getPrefix(const std::wstring& haystack, const std::vector& needles, bool CI); static std::wstring getSuffix(const std::wstring& haystack, const std::vector& needles, bool CI); static bool startsWithAny(const std::wstring& haystack, const std::vector& needles, bool CI); @@ -51,6 +54,10 @@ public: { return abs(f2 - f1) < .00000001; // TODO: kEpsilon? (see also ) } + + static std::string ltrim(const std::string &s); + static std::string rtrim(const std::string &s); + static std::string trim(const std::string &s); }; class TestUtility { diff --git a/b3view.pro b/b3view.pro index b720ab4..3ea4a54 100644 --- a/b3view.pro +++ b/b3view.pro @@ -9,14 +9,16 @@ SOURCES += main.cpp \ Debug.cpp \ View.cpp \ extlib/CGUITTFont.cpp \ - Utility.cpp + Utility.cpp \ + settings.cpp HEADERS += Engine.h \ EventHandler.h \ UserInterface.h \ Debug.h \ View.h \ extlib/CGUITTFont.h \ - Utility.h + Utility.h \ + settings.h CONFIG += warn_off # Irrlicht diff --git a/changelog.md b/changelog.md index 1e730d5..dde8248 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [git] - 2021-02-17 +### Added +- a settings system and related tests +- more string functions in Utility + +### Fixed +- Add a missing quote in TestUtility output. + ## [git] - 2020-07-30 (Poikilos) diff --git a/settings.cpp b/settings.cpp new file mode 100644 index 0000000..e0c2a49 --- /dev/null +++ b/settings.cpp @@ -0,0 +1,454 @@ +#include +#include +#include +#include // std::find + +#include + +#include "settings.h" +#include "Utility.h" +// #include "Debug.h" + +using namespace std; + +void Settings::init_default_symbols() { + this->init(" = ", "# "); +} + +void Settings::init(std::string assignmentOperator, std::string commentMark) +{ + this->ao_and_spacing = assignmentOperator; + this->cm_and_spacing = commentMark; + this->enable_autosave = true; +} + +Settings::Settings() +{ + this->init_default_symbols(); +} + +Settings::Settings(std::string confPath) +{ + this->init_default_symbols(); + this->path = confPath; + this->load(confPath); +} + +void Settings::clear() +{ + this->section = ""; + this->sections.clear(); + this->table.clear(); +} + +void Settings::clear_types() +{ + this->types.clear(); +} + +bool Settings::load(std::string path) +{ + this->section = ""; + this->path = path; + fstream newfile; + this->pre = ""; + newfile.open(path,ios::in); + std::string ao = this->ao_trimmed(); + std::string cm = this->cm_trimmed(); + if (newfile.is_open()){ + std::string line; + int lineN = 0; // Set this to 1 before use. + while(getline(newfile, line)) { + lineN += 1; + this->pre = this->path + ":" + std::to_string(lineN) + ": "; // must end with space for outputinspector + line = Utility::trim(line); + std::size_t signPos = line.find(ao); + std::string typeStr = "string"; + if ((line.length() >= cm.length()) && (line.substr(0, cm.length()) == cm)) { + // comment + } + else if (line.length() == 0) { + // blank + } + else if (signPos != std::string::npos) { + std::string name = Utility::trim(line.substr(0, signPos)); + std::string value = Utility::trim(line.substr(signPos+1)); + std::string::size_type iSz; + std::string::size_type fSz; + int valueI = std::stoi(value, &iSz); + float valueF = std::stof(value, &fSz); + // ^ radix (3rd param) default is 10 (base-10 number system) + cerr << name << std::endl; + cerr << " valueI length: " << iSz << std::endl; + cerr << " valueF length: " << fSz << std::endl; + if (fSz > iSz) { + typeStr = "float"; + } + else if (iSz == value.length()) { + typeStr = "int"; + } + else if (iSz > 0) { + cerr << this->pre << "WARNING: The file has a " + << typeStr << " for " << name << ", but " + << this->types[name] << " was expected." + << std::endl; + } + + this->types[name] = typeStr; + this->table[name] = value; + if (this->types.find(name) != this->types.end()) { + if (this->types[name] != typeStr) { + cerr << this->pre << "WARNING: The file has a " + << typeStr << " for " << name << ", but " + << this->types[name] << " was expected." + << std::endl; + } + } + if (this->section.length() > 0) { + this->sections[name] = this->section; + } + } + else { + if ( + (line.length() >= 3) + && Utility::startsWith(line, "[") + && Utility::endsWith(line, "]") + ) { + std::string section = Utility::trim(line.substr(1, line.length()-2)); + if (section.length() > 0) { + this->section = section; + } + else { + cerr << this->pre << "WARNING: The file has a blank section \"" + << line << "\" (expected section name)." + << std::endl; + } + } + else { + cerr << this->pre << "WARNING: \"" + << line << "\" is not understood (expected comment with '" + << cm <<"' (settable via this->setCM(commentMark))," + << " section enclosed in '[' and ']' or assignment containing '" + << ao << "' [settable via this->setAO(assignmentOperator)])." + << std::endl; + } + } + } + newfile.close(); + } + this->section = ""; + this->pre = ""; +} + +bool Settings::save(std::string path) +{ + this->path = path; + ofstream myfile; + myfile.open(path, ios::out); // default is ios_base::out + std::map::iterator it; + std::vector sectionNames; + sectionNames.push_back(" "); + for (it = this->sections.begin(); it != this->sections.end(); it++) { + if (std::find(sectionNames.begin(), sectionNames.end(), it->second) == sectionNames.end()) + sectionNames.push_back(it->second); + } + // Save each section consecutively, starting with variables that have no section. + for (std::vector::iterator vIt = sectionNames.begin() ; vIt != sectionNames.end(); ++vIt) { + if (*vIt != " ") { + myfile << "[" << *vIt << "]" << std::endl; + } + for (it = table.begin(); it != table.end(); it++) { + if (this->sections.find(it->first) != this->sections.end()) { + if (*vIt == this->sections[it->first]) { + myfile << it->first << this->ao_and_spacing << it->second << std::endl; + } + } + else if (*vIt == " ") { + // ^ " " is the global section + // (The variable is not in a section). + myfile << it->first << this->ao_and_spacing << it->second << std::endl; + } + } + } + myfile.close(); + return true; +} + +bool Settings::save() +{ + if (this->path.length() == 0) { + throw std::string("There is no path during save()."); + } + this->save(this->path); +} + +string Settings::ao_trimmed() +{ + Utility::trim(this->ao_and_spacing); +} + +string Settings::cm_trimmed() +{ + Utility::trim(this->cm_and_spacing); +} + +bool Settings::check_type(std::string typeStr, std::string name) +{ + std::string currentTypeStr = get_type_str(name); + if ((currentTypeStr != "") && (typeStr != currentTypeStr)) { + cerr << this->pre << "WARNING: settings.set got a(n) " + << typeStr << " (value \"" << this->table[name] << "\") for " + << name << ", but expected a(n) " + << currentTypeStr << "." + << std::endl; + return false; + } + return true; +} + +string Settings::get_type_str(string name) +{ + std::string currentTypeStr = ""; + if (this->types.find(name) != this->types.end()) { + currentTypeStr = this->types[name]; + } + return currentTypeStr; +} + +bool Settings::exists(string name) +{ + return (this->table.find(name) != this->table.end()); +} + +void Settings::set_ao_and_spacing(std::string assignmentOperator) +{ + this->ao_and_spacing = assignmentOperator; +} + +void Settings::set_cm_and_spacing(std::string commentMark) +{ + this->cm_and_spacing = commentMark; +} + +bool Settings::set_all_auto(std::map table) +{ + std::map::iterator it; + for (it = table.begin(); it != table.end(); it++) { + this->set_auto(it->first, it->second); + } +} + +void Settings::set_section(std::string sectionName) +{ + this->section = sectionName; +} + +void Settings::set_raw(std::string name, std::string value) { + std::string section = this->section; + // In any case below, never change the section of a variable + // that was already declared. + if (this->sections.find(name) != this->sections.end()) { + // Get use its section since it exists. + section = this->sections[name]; + } + else if (this->exists(name)) { + section = ""; + } + std::string previous = ""; + if (this->table.find(name) != this->table.end()) { + previous = this->table[name]; + } + this->table[name] = value; + if (section.length() > 0) { + this->sections[name] = section; + } + if (this->enable_autosave) { + if (previous != value) { + if (this->path.length() > 0) { + this->save(); + } + } + } +} + +bool Settings::set(std::string name, std::string value) +{ + bool match = this->check_type("string", name); + set_raw(name, value); + if (this->types.find(name) == this->types.end()) { + this->types[name] = "string"; + } + return match; +} + +bool Settings::set_int(std::string name, int value) +{ + bool match = this->check_type("int", name); + this->set_raw(name, std::to_string(value)); + if (this->types.find(name) == this->types.end()) { + this->types[name] = "int"; + } + return match; +} + +bool Settings::set_float(std::string name, irr::f32 value) +{ + bool match = this->check_type("float", name); + this->set_raw(name, std::to_string(value)); + if (this->types.find(name) == this->types.end()) { + this->types[name] = "float"; + } + return match; +} + +void Settings::set_auto(std::string name, std::string value) +{ + std::string typeStr = "string"; + if (this->types.find(name) != this->types.end()) { + std::string typeStr = this->types[name]; + } + if (typeStr == "int") { + std::string::size_type iSz; + int valueI = std::stoi(value, &iSz); + this->set_int(name, valueI); + } + else if (typeStr == "float") { + std::string::size_type fSz; + float valueF = std::stof(value, &fSz); + this->set_float(name, valueF); + } + else { + // Treat typeStr as string if blank + // (implement more types above to avoid errors in + // "set" which always assumes that string is the type). + this->set(name, value); + } +} + +string Settings::get(string name, bool& found) +{ + bool match = this->check_type("string", name); + if (this->table.find(name) == this->table.end()) { + found = false; + return ""; + } + found = true; + return this->table[name]; +} + +float Settings::get_float(string name, bool &found) +{ + bool match = this->check_type("float", name); + if (this->table.find(name) == this->table.end()) { + found = false; + return 0.0f; + } + found = true; + size_t sz; + std::string value = this->table[name]; + float v = std::stof(value, &sz); + if (sz != name.length()) { + cerr << this->pre << "WARNING: only \"" + << value.substr(0, sz) << "\" part of \"" + << value << "\" for the variable named \"" + << name << "\" is a number." + << endl; + } + return v; +} + +int Settings::get_int(string name, bool &found) +{ + bool match = this->check_type("int", name); + if (this->table.find(name) == this->table.end()) { + found = false; + return 0; + } + found = true; + size_t sz; + std::string value = this->table[name]; + int v = std::stoi(value, &sz); + if (sz != name.length()) { + cerr << this->pre << "WARNING: only \"" + << value.substr(0, sz) << "\" part of \"" + << value << "\" for the variable named \"" + << name << "\" is a number." + << endl; + } + return v; +} + + + + +///////////////////////// TESTS ///////////////////////// + +TestSettings::TestSettings() { + cerr << "TestSettings..." << std::flush; + assert_type_warning_str_then_int(); + assert_set("what", "bluedragon"); + assert_set("what", "greendragon"); + assert_get("what", "greendragon"); + assert_section_set_on_create(); + cerr << "OK" << std::endl; +} + +void TestSettings::assert_equal(const std::string subject, const std::string expectedResult) +{ + if (subject != expectedResult) { + cerr << "The test expected \"" << expectedResult << "\" but got \"" << subject << "\"" << std::endl; + } + assert(subject == expectedResult); +} + +void TestSettings::assert_set(const std::string &name, const std::string &value) +{ + settings.set(name, value); + bool found = false; + std::string result = settings.get(name, found); + assert_equal(result, value); + assert(found); +} + +void TestSettings::assert_get(const std::string &name, const std::string &expectedResult) +{ + bool found = false; + std::string result = settings.get(name, found); + assert(found); + assert_equal(result, expectedResult); +} + +void TestSettings::assert_type_warning_str_then_int() +{ + settings.clear(); + settings.set("username", "Poikilos"); + bool match = settings.set_int("username", 1); + assert(!match); + cerr << "(The warning above is expected during the test: expected string, got int)" << endl; +} + +void TestSettings::assert_section_set_on_create() +{ + std::string tmpPath = "test.conf"; + settings.clear(); + settings.set_int("a", 1); + settings.set_section("more"); + settings.set_int("b", 2); + settings.set_int("a", 3); + settings.save(tmpPath); + settings.clear(); + ifstream myfile(tmpPath); + if (myfile.is_open()) + { + std::string line; + assert(getline(myfile,line)); + assert_equal(line, "a = 3"); + assert(getline(myfile,line)); + assert_equal(line, "[more]"); + assert(getline(myfile,line)); + assert_equal(line, "b = 2"); + myfile.close(); + } +} + +static TestSettings testsettings; // Run tests (Creating the first instance runs the static constructor). diff --git a/settings.h b/settings.h new file mode 100644 index 0000000..ee7d03f --- /dev/null +++ b/settings.h @@ -0,0 +1,62 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include +#include + +class Settings +{ +private: + std::string path; + std::map table; + std::map types; + std::map sections; + std::string section; + std::string ao_and_spacing; // assignment operator + std::string cm_and_spacing; // comment mark + std::string pre; // debug prefix such as "filename:lineNumber: " (must end with space for outputinspector) + void init(std::string assignmentOperator, std::string commentMark); + void init_default_symbols(); + +public: + bool enable_autosave; + + Settings(); + Settings(std::string confPath); + void clear(); + void clear_types(); + bool load(std::string path); + bool save(std::string path); + bool save(); + std::string ao_trimmed(); + std::string cm_trimmed(); + bool check_type(std::string typeStr, std::string name); + std::string get_type_str(std::string name); + bool exists(std::string name); + void set_ao_and_spacing(std::string assignmentOperator); + void set_cm_and_spacing(std::string commentMark); + bool set_all_auto(std::map table); + void set_section(std::string sectionName); + void set_raw(std::string name, std::string value); + bool set(std::string name, std::string value); + bool set_float(std::string name, irr::f32 value); + bool set_int(std::string name, int value); + void set_auto(std::string name, std::string value); + std::string get(std::string name, bool& found); + float get_float(std::string name, bool& found); + int get_int(std::string name, bool& found); +}; + +class TestSettings { + Settings settings; +public: + TestSettings(); + void assert_equal(const std::string subject, const std::string expectedResult); + void assert_set(const std::string& name, const std::string& value); + void assert_get(const std::string& name, const std::string& expectedResult); + void assert_type_warning_str_then_int(); + void assert_section_set_on_create(); +}; + +#endif // SETTINGS_H