Add a settings system and related tests...

...See changelog for more.
master
poikilos 2021-02-17 18:55:39 -05:00
parent ff44991f55
commit 98ad5d1457
8 changed files with 591 additions and 17 deletions

View File

@ -279,6 +279,7 @@ s32 Engine::getNumberOfVertices()
Engine::Engine()
{
settings.set_int("max_recent", 10);
// For monitoring single press: see
// <http://irrlicht.sourceforge.net/forum/viewtopic.php?p=210744>
for (u32 i = 0; i < KEY_KEY_CODES_COUNT; ++i)

View File

@ -13,6 +13,7 @@ class View;
#include "EventHandler.h"
#include "extlib/CGUITTFont.h"
#include <irrlicht/irrlicht.h>
#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;

View File

@ -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<std::wstring>& 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<typename T>
// 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).

View File

@ -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<std::wstring>& needles, bool CI);
static std::wstring getSuffix(const std::wstring& haystack, const std::vector<std::wstring>& needles, bool CI);
static bool startsWithAny(const std::wstring& haystack, const std::vector<std::wstring>& needles, bool CI);
@ -51,6 +54,10 @@ public:
{
return abs(f2 - f1) < .00000001; // TODO: kEpsilon? (see also <https://en.wikipedia.org/wiki/Machine_epsilon#How_to_determine_machine_epsilon>)
}
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 {

View File

@ -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

View File

@ -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)

454
settings.cpp Normal file
View File

@ -0,0 +1,454 @@
#include <fstream>
#include <iostream>
#include <vector>
#include <algorithm> // std::find
#include <assert.h>
#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<std::string, std::string>::iterator it;
std::vector<std::string> 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<std::string>::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<std::string, std::string> table)
{
std::map<std::string, std::string>::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).

62
settings.h Normal file
View File

@ -0,0 +1,62 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <string>
#include <map>
#include <irrlicht/irrlicht.h>
class Settings
{
private:
std::string path;
std::map<std::string, std::string> table;
std::map<std::string, std::string> types;
std::map<std::string, std::string> 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<std::string, std::string> 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