I reinvented JSON :)

Published February 01, 2017
Advertisement

As always happens when I'm starting a new game, I've started a new level editor. This one, however, is based on my [font='courier new']Gx[/font] and [font='courier new']QGx[/font] frameworks and so I'm hoping to keep it generic enough that it might be the last one I ever have to write (yeah, right) :)

I'm going to need to have human-readable configuration files for the editor and I hate working with XML, especially with the Qt interfaces. I also find it very hard to read. So I've reimplemented an old system I invented years ago as part of the [font='courier new']Gx[/font] framework, called [font='courier new']Gx::Settings[/font].

It is more like JSON than XML. Here's an example of a script:

window{ width = 640; height = 480; maximized = false; controls { title = "This is a literal"; }}levels{ "C:/levels/level1.dat"; "C:/levels/level2.dat"; "C:/levels/level3.dat";}Each entry starts with either an ID (starts with letter or underscore, contains letters, numbers and underscores) or a literal. In the second case, an unnamed node is created with the provided value. In the former case, you have an optional assignment of a literal to the value, then an optional brace enclosed list of entries that become child nodes of the node.

The semicolons are pretty much ignored and are there to aid readability. They are required because two string literals separated only by whitespace are concatenated into one string, so in the "levels" example above, they need to be there.

The code-side interface to this is a recursive [font='courier new']Gx::Settings[/font] object. This is a PIMPL-based class implementing the rule of five:

namespace Gx{class Settings{public: Settings(); Settings(const Settings &s); Settings(Settings &&s); ~Settings(); Settings &operator=(const Settings &s); Settings &operator=(Settings &&s); // snipprivate: class Rep; Rep *rep;};}[font='courier new']Gx::Settings[/font] offers a non-const-only [font='courier new']operator[](const std::string &id)[/font] which either finds or creates a new node. You can also assign a [font='courier new']std::string[/font] to it, which sets its value. There is then a template assign method that allows you to directly assign any supported type.

Similarly there are template [font='courier new']value()[/font] and [font='courier new']canConvert()[/font] methods for retrieving the value directly into a supported type.

It is based upon [font='courier new']Gx::lexical_cast[/font] which uses the standard-library stringstreams for doing conversions, so any type that provides overloads for the stream inseration and extraction operators can be used.


void f(){ Gx::Settings root; Gx::Vec3 v(1, 2, 3); // assume stream operators provided for type root["hello"]["world"] = v; if(root["hello"]["world"].canConvert()) { Gx::Vec3 t = root["hello"]["world"].value(); }}[font='courier new']Gx::Settings[/font] also provides [font='courier new']iterator[/font] and [font='courier new']const_iterator[/font] for traversing its children. From the "levels" example above:


void listLevels(Gx::Settings &root){ for(auto &i: root["levels"]) { std::cout << i.value() << "\n"; }}The child nodes are stored in each parent in the order they are created. Calling the [font='courier new']operator[][/font] with an empty string argument appends an unnamed node to the end of the list.

Finally there is a static method for loading from a stream. The following loads a configuration file and prints it recursively as a tree:

void dump(Gx::Settings &node, int indent){ for(const auto &i: node) { std::cout << i.id() << " = " << i.value() << "\n"; dump(node, indent + 4); }}void f(){ std::ifstream is("data.txt"); if(is.is_open()) { std::string error; Gx::Settings root = Gx::Settings::fromStream(is, error); if(root.empty()) { std::cerr << "Error " << error << "\n"; return; } dump(root, 0); }}This is the entire header for [font='courier new']Gx::Settings[/font] itself. There are also some internal-only files that deal with parsing and constructing the node tree from a file, but they obviously don't form any of the public interface of the library.

namespace Gx{class Settings{public: class iterator { public: iterator &operator++(){ ++index; return *this; } iterator operator++(int){ return iterator(parent, index++); } iterator &operator--(){ --index; return *this; } iterator operator--(int){ return iterator(parent, index--); } iterator operator+(unsigned int s) const { return iterator(parent, index + s); } iterator operator-(unsigned int s) const { return iterator(parent, index - s); } bool operator==(const iterator &o) const { return parent == o.parent && index == o.index; } bool operator!=(const iterator &o) const { return parent != o.parent || index != o.index; } Settings &operator*() const; Settings *operator->() const; private: friend class Settings; iterator(Settings *parent, unsigned int index) : parent(parent), index(index) { } Settings *parent; unsigned int index; }; class const_iterator { public: const_iterator &operator++(){ ++index; return *this; } const_iterator operator++(int){ return const_iterator(parent, index++); } const_iterator &operator--(){ --index; return *this; } const_iterator operator--(int){ return const_iterator(parent, index--); } const_iterator operator+(unsigned int s) const { return const_iterator(parent, index + s); } const_iterator operator-(unsigned int s) const { return const_iterator(parent, index - s); } bool operator==(const const_iterator &o) const { return parent == o.parent && index == o.index; } bool operator!=(const const_iterator &o) const { return parent != o.parent || index != o.index; } const Settings &operator*() const; const Settings *operator->() const; private: friend class Settings; const_iterator(const Settings *parent, unsigned int index) : parent(parent), index(index) { } const Settings *parent; unsigned int index; }; Settings(); Settings(const Settings &s); Settings(Settings &&s); ~Settings(); Settings &operator=(const Settings &s); Settings &operator=(Settings &&s); Settings &operator[](const std::string &id); bool contains(const std::string &id) const; bool empty() const; Settings &operator=(const std::string &value); Settings &operator=(const char *value); template Settings &operator=(const T &t){ return (*this) = lexical_cast(t); } template T value() const { return lexical_cast(val()); } template bool canConvert() const { return can_lexical_cast(val()); } iterator begin(); iterator end(); const_iterator begin() const; const_iterator end() const; std::string id() const; static Settings fromStream(std::istream &is, std::string &error);private: std::string val() const; class Rep; Rep *rep;};}So we have both easily-readable and editable text file sources for defining arbitrary hierachical data and a fairly nice and neat interface for manipulating this data in the host application and a simple way to extend it to support custom types by just requiring stream insertion and extraction operators to convert a custom type to and from a string.

And so [font='courier new']Gx[/font] progresses. Time to get back into the editor now :)

Thanks for stopping by.

Previous Entry Gx::Camera wafflings
Next Entry New Game
5 likes 2 comments

Comments

Navyman

Why would you have the window size in the xml and not a flexible var?

Are these default values?

February 01, 2017 07:44 PM
Aardvajk

Why would you have the window size in the xml and not a flexible var?
Are these default values?


Just an example, not an actual file. What do you mean, a flexible var?
February 02, 2017 07:07 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement