The open source repository for the action RPG game in development by Sig Productions titled 'Adventures in Lestoria'! https://forums.lestoria.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
AdventuresInLestoria/Adventures in Lestoria/olcUTIL_DataFile.h

633 lines
20 KiB

/*
OneLoneCoder - DataFile v1.00
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An "easy to use" serialisation/deserialisation class that yields
human readable hierachical files.
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2018 - 2024 OneLoneCoder.com
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions or derivations of source code must retain the above
copyright notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce
the above copyright notice. This list of conditions and the following
disclaimer must be reproduced in the documentation and/or other
materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Links
~~~~~
YouTube: https://www.youtube.com/javidx9
Discord: https://discord.gg/WhwHUMV
Twitter: https://www.twitter.com/javidx9
Twitch: https://www.twitch.tv/javidx9
GitHub: https://www.github.com/onelonecoder
Homepage: https://www.onelonecoder.com
Author
~~~~~~
David Barr, aka javidx9, <EFBFBD>OneLoneCoder 2019, 2020, 2021, 2022
*/
#pragma once
#include "olcPixelGameEngine.h"
#include <iostream>
#include <string>
#include <unordered_map>
#include <functional>
#include <fstream>
#include <stack>
#include <sstream>
#include <numeric>
#include "Error.h"
using namespace std::literals;
int operator ""_I(const char*key,std::size_t len);
namespace olc::utils
{
class datafile
{
public:
enum class OverwriteMode{
NO_OVERWRITE,
OVERWRITE,
};
inline datafile() = default;
inline static bool DEBUG_ACCESS_OPTIONS=false;
inline static bool INITIAL_SETUP_COMPLETE=false;
public:
// Sets the String Value of a Property (for a given index)
inline void SetString(const std::string& sString, const size_t nItem = 0)
{
if (nItem >= m_vContent.size())
m_vContent.resize(nItem + 1);
m_vContent[nItem] = sString;
}
// Retrieves the String Value of a Property (for a given index) or ""
inline const std::string&GetString(const size_t nItem = 0) const
{
if (nItem >= m_vContent.size()){
ERR("WARNING! Accesing out-of-bounds list item "<<nItem<<" of "<<lastAccessedProperty<<"!");
return BLANK;
}
else {
return m_vContent[nItem];
}
}
// Retrieves the String Value of a Property, to include all values that are normally separated by the list separator.
inline std::string GetFullString() const
{
return std::accumulate(m_vContent.begin(),m_vContent.end(),""s,[](std::string str,const std::string&data){
if(str.size()==0){
return std::move(str)+data;
}else{
return std::move(str)+", "+data;
}
});
}
// Retrieves the Real Value of a Property (for a given index) or 0.0
inline const float GetReal(const size_t nItem = 0) const
{
return std::stof(GetString(nItem).c_str());
}
// Sets the Real Value of a Property (for a given index)
inline void SetReal(const float d, const size_t nItem = 0)
{
SetString(std::to_string(d), nItem);
}
// Retrieves the Integer Value of a Property (for a given index) or 0
inline const int64_t GetBigInt(const size_t nItem = 0) const
{
return std::stoll(GetString(nItem).c_str());
}
// Retrieves the Integer Value of a Property (for a given index) or 0
inline const int32_t GetInt(const size_t nItem = 0) const
{
return std::stoi(GetString(nItem).c_str());
}
// Retrieves the Integer Value of a Property (for a given index) or 0
inline const vf2d GetVf2d(const size_t nItem = 0) const
{
return {GetReal(0),GetReal(1)};
}
// Retrieves the Boolean Value of a Property (for a given index) or false
inline const bool GetBool(const size_t nItem = 0) const
{
return GetString(nItem).starts_with('T')||GetString(nItem).starts_with('t');
}
// Sets the Boolean Value of a Property
inline void SetBool(const bool b, const size_t nItem = 0)
{
b?SetString("True",nItem):SetString("False",nItem);
}
// Sets the Integer Value of a Property (for a given index)
inline void SetInt(const int32_t n, const size_t nItem = 0)
{
SetString(std::to_string(n), nItem);
}
// Sets the Long Integer Value of a Property (for a given index)
inline void SetBigInt(const int64_t n, const size_t nItem = 0)
{
SetString(std::to_string(n), nItem);
}
inline Pixel GetPixel(const size_t nItem = 0){
return {uint8_t(GetInt(nItem)),uint8_t(GetInt(nItem+1)),uint8_t(GetInt(nItem+2)),uint8_t(GetInt(nItem+3))};
}
// Returns the number of Values a property consists of
inline size_t GetValueCount() const
{
return m_vContent.size();
}
inline const std::vector<std::string>&GetValues()const
{
return m_vContent;
}
inline const std::unordered_map<std::string,size_t>&GetKeys()const{
return m_mapObjects;
}
//This function is slightly expensive due to a filtering function required to remove all comments!
inline std::vector<std::pair<std::string,datafile>> GetOrderedKeys(){
std::vector<std::pair<std::string,datafile>>orderedKeys;
std::copy_if(m_vecObjects.begin(),m_vecObjects.end(),std::back_inserter(orderedKeys),[&](const std::pair<std::string,datafile>&data){return !datafile::IsComment(data);});
return orderedKeys;
}
// Checks if a property exists - useful to avoid creating properties
// via reading them, though non-essential
inline bool HasProperty(const std::string& sName)
{
size_t x = sName.find_first_of('.');
if (x != std::string::npos)
{
std::string sProperty = sName.substr(0, x);
if(HasProperty(sProperty)){
return GetProperty(sName.substr(0, x)).HasProperty(sName.substr(x + 1, sName.size()));
}else{
return false;
}
}else{
return m_mapObjects.count(sName) > 0;
}
}
// Access a datafile via a convenient name - "root.node.something.property"
inline datafile& GetProperty(const std::string& name)
{
if(DEBUG_ACCESS_OPTIONS)LOG(std::format("Accessing Property {}",name));
size_t x = name.find_first_of('.');
if (x != std::string::npos)
{
std::string sProperty = name.substr(0, x);
lastAccessedProperty=sProperty;
if (HasProperty(sProperty))
return operator[](sProperty).GetProperty(name.substr(x + 1, name.size()));
else {
ERR("WARNING! Could not read Property " << sProperty << "! Potential bugs may occur.")
return operator[](sProperty);
}
}
else
{
return operator[](name);
}
}
// Access a numbered element - "node[23]", or "root[56].node"
inline datafile& GetIndexedProperty(const std::string& name, const size_t nIndex)
{
return GetProperty(name + "[" + std::to_string(nIndex) + "]");
}
public:
// Writes a "datafile" node (and all of its child nodes and properties) recursively
// to a file.
inline static bool Write(const datafile& n, const std::string& sFileName, const std::string& sIndent = "\t", const char sListSep = ',')
{
// Cache indentation level
size_t nIndentCount = 0;
// Cache sperator string for convenience
std::string sSeperator = std::string(1, sListSep) + " ";
// Fully specified lambda, because this lambda is recursive!
std::function<void(const datafile&, std::ofstream&)> write = [&](const datafile& n, std::ofstream& file)
{
// Lambda creates string given indentation preferences
auto indent = [&](const std::string& sString, const size_t nCount)
{
std::string sOut;
for (size_t n = 0; n < nCount; n++) sOut += sString;
return sOut;
};
// Iterate through each property of this node
for (auto const& property : n.m_vecObjects)
{
// Does property contain any sub objects?
if (property.second.m_vecObjects.empty())
{
// No, so it's an assigned field and should just be written. If the property
// is flagged as comment, it has no assignment potential. First write the
// property name
file << indent(sIndent, nIndentCount) << property.first << (property.second.m_bIsComment ? "" : " = ");
// Second, write the property value (or values, seperated by provided
// separation charater
size_t nItems = property.second.GetValueCount();
for (size_t i = 0; i < property.second.GetValueCount(); i++)
{
// If the Value being written, in string form, contains the separation
// character, then the value must be written inside quotation marks. Note,
// that if the Value is the last of a list of Values for a property, it is
// not suffixed with the separator
size_t x = property.second.GetString(i).find_first_of(sListSep);
if (x != std::string::npos)
{
// Value contains separator, so wrap in quotes
file << "\"" << property.second.GetString(i) << "\"" << ((nItems > 1) ? sSeperator : "");
}
else
{
// Value does not contain separator, so just write out
file << property.second.GetString(i) << ((nItems > 1) ? sSeperator : "");
}
nItems--;
}
// Property written, move to next line
file << "\n";
}
else
{
// Yes, property has properties of its own, so it's a node
// Force a new line and write out the node's name
file << "\n" << indent(sIndent, nIndentCount) << property.first << "\n";
// Open braces, and update indentation
file << indent(sIndent, nIndentCount) << "{\n";
nIndentCount++;
// Recursively write that node
write(property.second, file);
// Node written, so close braces
file << indent(sIndent, nIndentCount) << "}\n\n";
}
}
// We've finished writing out a node, regardless of state, our indentation
// must decrease, unless we're top level
if (nIndentCount > 0) nIndentCount--;
};
// Start Here! Open the file for writing
std::ofstream file(sFileName);
if (file.is_open())
{
// Write the file starting form the supplied node
write(n, file);
return true;
}
return false;
}
inline static bool Read(datafile& n, const std::string& sFileName, const char sListSep = ',', const OverwriteMode mode=OverwriteMode::NO_OVERWRITE)
{
bool previousSetupState=INITIAL_SETUP_COMPLETE;
INITIAL_SETUP_COMPLETE=false;
// Open the file!
std::ifstream file(sFileName);
if (file.is_open())
{
// These variables are outside of the read loop, as we will
// need to refer to previous iteration values in certain conditions
std::string sPropName = "";
std::string sPropValue = "";
// The file is fundamentally structured as a stack, so we will read it
// in a such, but note the data structure in memory is not explicitly
// stored in a stack, but one is constructed implicitly via the nodes
// owning other nodes (aka a tree)
// I dont want to accidentally create copies all over the place, nor do
// I want to use pointer syntax, so being a bit different and stupidly
// using std::reference_wrapper, so I can store references to datafile
// nodes in a std::container.
std::stack<std::reference_wrapper<datafile>> stkPath;
stkPath.push(n);
// Read file line by line and process
while (!file.eof())
{
// Read line
std::string line;
std::getline(file, line);
// This little lambda removes whitespace from
// beginning and end of supplied string
auto trim = [](std::string& s)
{
s.erase(0, s.find_first_not_of(" \t\n\r\f\v"));
s.erase(s.find_last_not_of(" \t\n\r\f\v") + 1);
};
trim(line);
// If line has content
if (!line.empty())
{
// Test if its a comment...
if (line[0] == '#')
{
// ...it is a comment, so ignore
datafile comment;
comment.m_bIsComment = true;
stkPath.top().get().m_vecObjects.push_back({ line, comment });
}
else
{
// ...it is content, so parse. Firstly, find if the line
// contains an assignment. If it does then it's a property...
size_t x = line.find_first_of('=');
if (x != std::string::npos)
{
// ...so split up the property into a name, and its values!
// Extract the property name, which is all characters up to
// first assignment, trim any whitespace from ends
sPropName = line.substr(0, x);
trim(sPropName);
auto&top=stkPath.top().get();
if(mode==OverwriteMode::NO_OVERWRITE&&stkPath.top().get().HasProperty(sPropName))ERR(std::format("WARNING! Duplicate key found! Key {} already exists! Duplicate line: {}",sPropName,line));
// Extract the property value, which is all characters after
// the first assignment operator, trim any whitespace from ends
sPropValue = line.substr(x + 1, line.size());
trim(sPropValue);
// The value may be in list form: a, b, c, d, e, f etc and some of those
// elements may exist in quotes a, b, c, "d, e", f. So we need to iterate
// character by character and break up the value
bool bInQuotes = false;
bool bEscapeChar = false;
std::string sToken;
size_t nTokenCount = 0;
for (const auto c : sPropValue)
{
if (bEscapeChar&&c == 'n') //Parse a newline.
{
sToken.append(1, '\n');
bEscapeChar=false;
continue;
}
bEscapeChar=false;
// Is character a quote...
if (c == '\"')
{
// ...yes, so toggle quote state
bInQuotes = !bInQuotes;
}
else
if (c == '\\')
{
// ...yes, so toggle quote state
bEscapeChar=true;
}
else
{
// ...no, so proceed creating token. If we are in quote state
// then just append characters until we exit quote state.
if (bInQuotes)
{
sToken.append(1, c);
}
else
{
// Is the character our seperator? If it is
if (c == sListSep)
{
// Clean up the token
trim(sToken);
// Add it to the vector of values for this property
stkPath.top().get()[sPropName].SetString(sToken, nTokenCount);
// Reset our token state
sToken.clear();
nTokenCount++;
}
else
{
// It isnt, so just append to token
sToken.append(1, c);
}
}
}
}
// Any residual characters at this point just make up the final token,
// so clean it up and add it to the vector of values
if (!sToken.empty())
{
trim(sToken);
stkPath.top().get()[sPropName].SetString(sToken, nTokenCount);
}
}
else
{
// ...but if it doesnt, then it's something structural
if (line[0] == '{')
{
// Open brace, so push this node to stack, subsequent properties
// will belong to the new node
stkPath.push(stkPath.top().get()[sPropName]);
}
else
{
if (line[0] == '}')
{
// Close brace, so this node has been defined, pop it from the
// stack
stkPath.pop();
}
else
{
// Line is a property with no assignment. Who knows whether this is useful,
// but we can simply add it as a valueless property...
sPropName = line;
// ...actually it is useful, as valuless properties are typically
// going to be the names of new datafile nodes on the next iteration
}
}
}
}
}
}
// Close and exit!
file.close();
INITIAL_SETUP_COMPLETE=previousSetupState;
return true;
}
// File not found, so fail
ERR("WARNING! Could not open file "<<sFileName<<"!");
INITIAL_SETUP_COMPLETE=previousSetupState;
return false;
}
public:
inline void Reset(){
INITIAL_SETUP_COMPLETE=false;
DEBUG_ACCESS_OPTIONS=false;
m_mapObjects.clear();
m_vecObjects.clear();
m_vContent.clear();
lastAccessedProperty="";
BLANK="";
m_bIsComment=false;
}
inline datafile& operator[](const std::string& name)
{
// Check if this "node"'s map already contains an object with this name...
if (m_mapObjects.count(name) == 0)
{
if(INITIAL_SETUP_COMPLETE){
ERR("WARNING! Key "<<name<<" does not exist in datafile!") //We're not going to allow manual creation of nodes.
}
// ...it did not! So create this object in the map. First get a vector id
// and link it with the name in the unordered_map
m_mapObjects[name] = m_vecObjects.size();
// then creating the new, blank object in the vector of objects
m_vecObjects.push_back({ name, datafile() });
}
// ...it exists! so return the object, by getting its index from the map, and using that
// index to look up a vector element.
return m_vecObjects[m_mapObjects[name]].second;
}
inline auto begin(){
return GetKeys().begin();
}
inline auto end(){
return GetKeys().end();
}
private:
// The "list of strings" that make up a property value
std::vector<std::string> m_vContent;
// Linkage to create "ordered" unordered_map. We have a vector of
// "properties", and the index to a specific element is mapped.
std::vector<std::pair<std::string, datafile>> m_vecObjects;
std::unordered_map<std::string, size_t> m_mapObjects;
inline static std::string lastAccessedProperty="";
inline static std::string BLANK="";
inline static bool IsComment(const std::pair<std::string,datafile>&data){
return data.first.length()>0&&data.first[0]=='#';
}
protected:
// Used to identify if a property is a comment or not, not user facing
bool m_bIsComment = false;
};
class datafilestringdata
{
std::reference_wrapper<datafile>data;
std::string key;
public:
inline datafilestringdata(datafile&dat,std::string key)
:data(dat),key(key){};
std::string operator[](int index){return data.get().GetProperty(key).GetString(index);};
inline std::string concat(){
std::string finalStr="";
for(const std::string&str:data.get().GetProperty(key).GetValues()){
if(finalStr.length()>0)finalStr+=", ";
finalStr+=str;
}
return finalStr;
}
};
class datafilebooldata
{
std::reference_wrapper<datafile>data;
std::string key;
public:
inline datafilebooldata(datafile&dat,std::string key)
:data(dat),key(key){};
int operator[](int index){
return data.get().GetProperty(key).GetBool(index);
};
};
class datafileintdata
{
std::reference_wrapper<datafile>data;
std::string key;
public:
inline datafileintdata(datafile&dat,std::string key)
:data(dat),key(key){};
int operator[](int index){
return data.get().GetProperty(key).GetInt(index);
};
};
class datafilefloatdata
{
std::reference_wrapper<datafile>data;
std::string key;
public:
inline datafilefloatdata(datafile&dat,std::string key)
:data(dat),key(key){};
float operator[](int index){return float(data.get().GetProperty(key).GetReal(index));};
};
class datafiledoubledata
{
std::reference_wrapper<datafile>data;
std::string key;
public:
inline datafiledoubledata(datafile&dat,std::string key)
:data(dat),key(key){};
double operator[](int index){return data.get().GetProperty(key).GetReal(index);};
};
}