Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xml improvements #831

Merged
merged 12 commits into from
Mar 15, 2021
39 changes: 18 additions & 21 deletions OPHD/Common.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#include "Common.h"
#include "Constants.h"
#include "StructureManager.h"
#include "XmlSerializer.h"

#include "Things/Structures/Structure.h"
#include "Things/Structures/Warehouse.h"

#include <NAS2D/Utility.h>
#include <NAS2D/Filesystem.h>
#include <NAS2D/Xml/XmlDocument.h>
#include <NAS2D/Xml/XmlElement.h>

Expand All @@ -30,11 +30,13 @@ const std::map<Difficulty, std::string> DIFFICULTY_STRING_TABLE
{ Difficulty::Hard, constants::DIFFICULTY_HARD },
};


std::string StringFromDifficulty(const Difficulty& difficulty)
{
return DIFFICULTY_STRING_TABLE.at(difficulty);
}


Difficulty DifficultyFromString(std::string difficultyStr)
{
difficultyStr = NAS2D::toLowercase(difficultyStr);
Expand Down Expand Up @@ -431,35 +433,30 @@ bool doYesNoMessage(const std::string& title, const std::string msg)
}


void checkSavegameVersion(const std::string& filename)
{
// openSavegame checks version number after opening file
openSavegame(filename);
}


/**
* Checks a savegame version.
*
* Open a saved game and validate version.
*
* \throws Throws a std::runtime_error if there are any errors with a savegame version, formation or missing root nodes.
*
* \fixme Find a more efficient way to do this.
*/
void checkSavegameVersion(const std::string& filename)
NAS2D::Xml::XmlDocument openSavegame(const std::string& filename)
{
auto xmlFile = Utility<Filesystem>::get().open(filename);
auto xmlDocument = openXmlFile(filename, constants::SAVE_GAME_ROOT_NODE);

NAS2D::Xml::XmlDocument doc;
doc.parse(xmlFile.raw_bytes());
if (doc.error())
{
throw std::runtime_error("Malformed savegame ('" + filename + "'). Error on Row " + std::to_string(doc.errorRow()) + ", Column " + std::to_string(doc.errorCol()) + ": " + doc.errorDesc());
}
auto savegameVersion = xmlDocument.firstChildElement(constants::SAVE_GAME_ROOT_NODE)->attribute("version");

NAS2D::Xml::XmlElement* root = doc.firstChildElement(constants::SAVE_GAME_ROOT_NODE);
if (root == nullptr)
if (savegameVersion != constants::SAVE_GAME_VERSION)
{
throw std::runtime_error("Root element in '" + filename + "' is not '" + constants::SAVE_GAME_ROOT_NODE + "'.");
throw std::runtime_error("Savegame version mismatch: '" + filename + "'. Expected " + constants::SAVE_GAME_VERSION + ", found " + savegameVersion + ".");
}

std::string sg_version = root->attribute("version");
if (sg_version != constants::SAVE_GAME_VERSION)
{
throw std::runtime_error("Savegame version mismatch: '" + filename + "'. Expected " + constants::SAVE_GAME_VERSION + ", found " + sg_version + ".");
}
return xmlDocument;
}


Expand Down
5 changes: 5 additions & 0 deletions OPHD/Common.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
#include <string>
#include <vector>

namespace NAS2D::Xml {
class XmlDocument;
}

enum class StructureState;

enum class Difficulty
Expand Down Expand Up @@ -308,6 +312,7 @@ void doAlertMessage(const std::string& title, const std::string& msg);
bool doYesNoMessage(const std::string& title, const std::string msg);

void checkSavegameVersion(const std::string& filename);
NAS2D::Xml::XmlDocument openSavegame(const std::string& filename);

NAS2D::StringList split_string(const char *str, char delim);

Expand Down
27 changes: 4 additions & 23 deletions OPHD/States/MapViewStateIO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "../StructureCatalogue.h"
#include "../StructureManager.h"
#include "../Map/TileMap.h"
#include "../XmlSerializer.h"

#include <NAS2D/Utility.h>
#include <NAS2D/Filesystem.h>
Expand Down Expand Up @@ -155,36 +156,16 @@ void MapViewState::load(const std::string& filePath)
throw std::runtime_error("File '" + filePath + "' was not found.");
}

auto xmlFile = Utility<Filesystem>::get().open(filePath);

XmlDocument doc;

// Load the XML document and handle any errors if occuring
doc.parse(xmlFile.raw_bytes());
if (doc.error())
{
throw std::runtime_error("Malformed savegame ('" + filePath + "'). Error on Row " + std::to_string(doc.errorRow()) + ", Column " + std::to_string(doc.errorCol()) + ": " + doc.errorDesc());
}

XmlElement* root = doc.firstChildElement(constants::SAVE_GAME_ROOT_NODE);
if (root == nullptr)
{
throw std::runtime_error("Root element in '" + filePath + "' is not '" + constants::SAVE_GAME_ROOT_NODE + "'.");
}

std::string sg_version = root->attribute("version");
if (sg_version != constants::SAVE_GAME_VERSION)
{
throw std::runtime_error("Savegame version mismatch: '" + filePath + "'. Expected " + constants::SAVE_GAME_VERSION + ", found " + sg_version + ".");
}

scrubRobotList();
Utility<StructureManager>::get().dropAllStructures();
ccLocation() = CcNotPlaced;

delete mTileMap;
mTileMap = nullptr;

auto xmlDocument = openSavegame(filePath);
auto* root = xmlDocument.firstChildElement(constants::SAVE_GAME_ROOT_NODE);

XmlElement* map = root->firstChildElement("properties");
XmlAttribute* attribute = map->firstAttribute();
while (attribute)
Expand Down
117 changes: 117 additions & 0 deletions OPHD/States/Planet.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#include "Planet.h"

#include "../Constants.h"
#include "../XmlSerializer.h"

#include <NAS2D/Utility.h>
#include <NAS2D/EventHandler.h>
#include <NAS2D/Renderer/Renderer.h>
#include <NAS2D/Renderer/Rectangle.h>
#include <NAS2D/Xml/Xml.h>

#include <stdexcept>

Expand Down Expand Up @@ -66,3 +68,118 @@ void Planet::update()
const auto spriteFrameOffset = NAS2D::Point{mTick % 8 * PlanetSize.x, ((mTick % 64) / 8) * PlanetSize.y};
renderer.drawSubImage(mImage, mPosition, NAS2D::Rectangle<int>::Create(spriteFrameOffset, PlanetSize));
}

namespace
{
Planet::Attributes parsePlanet(const NAS2D::Xml::XmlElement* xmlNode);

void parseElementValue(Planet::PlanetType& destination, const NAS2D::Xml::XmlElement* element);
void parseElementValue(Planet::Hostility& destination, const NAS2D::Xml::XmlElement* element);

const std::unordered_map<std::string, Planet::PlanetType> planetTypeTable
{
{"None", Planet::PlanetType::None},
{"Mercury", Planet::PlanetType::Mercury},
{"Mars", Planet::PlanetType::Mars},
{"Ganymede", Planet::PlanetType::Ganymede}
};

const std::unordered_map<std::string, Planet::Hostility> hostilityTable
{
{"None", Planet::Hostility::None},
{"Low", Planet::Hostility::Low},
{"Medium", Planet::Hostility::Medium},
{"High", Planet::Hostility::High}
};


void parseElementValue(Planet::PlanetType& destination, const NAS2D::Xml::XmlElement* element)
{
destination = stringToEnum(planetTypeTable, element->getText());
}


void parseElementValue(Planet::Hostility& destination, const NAS2D::Xml::XmlElement* element)
{
destination = stringToEnum(hostilityTable, element->getText());
}
}


std::vector<Planet::Attributes> parsePlanetAttributes()
{
const std::string rootElementName("Planets");
auto xmlDocument = openXmlFile("planets/PlanetAttributes.xml", rootElementName);

std::vector<Planet::Attributes> planetAttributes;

auto rootElement = xmlDocument.firstChildElement(rootElementName);
for (const auto* node = rootElement->iterateChildren(nullptr);
node != nullptr;
node = rootElement->iterateChildren(node))
{
std::string elementName("Planet");
if (node->value() != elementName)
{
throw std::runtime_error(xmlDocument.value() + " missing " + elementName + " tag");
}
planetAttributes.push_back(parsePlanet(node->toElement()));
}

return planetAttributes;
}


namespace
{
Planet::Attributes parsePlanet(const NAS2D::Xml::XmlElement* xmlNode)
{
Comment on lines +133 to +136
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that it matters, though you could potentially have put this in the first unnamed namespace, possibly even replacing the function prototype. Though there is nothing wrong about re-opening an unnamed namespace and keeping the implementation separate.

Planet::Attributes attributes;

for (const auto* node = xmlNode->iterateChildren(nullptr);
node != nullptr;
node = xmlNode->iterateChildren(node))
{
const auto* element = node->toElement();

if (element->value() == "PlanetType")
{
parseElementValue(attributes.type, element);
}
else if (element->value() == "ImagePath")
{
::parseElementValue(attributes.imagePath, element);
}
Comment on lines +145 to +152
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yes, I see the discrepancy with the ::. It seems you have two functions parseElementValue, defined in different namespaces. That could get confusing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review. My goal was to keep the function names the same. I reasoned it might be possible to use a table to match argument names to locations in the structure to remove the if / else logic statements to place each value. I thought doing this would be easier if function overloads / templates were used. It gets complicated because of the detailed knowledge required about the enum to parse it properly, so the local module will need to define overloads for which enums it wants to support parsing. If this seems silly, I'm OK if the enum parse functions have custom names...

I think it might be worth trying to limit enums when possible when parsing to simplify. We could just represent hostility as a scale of 1 to 10 using an int value instead of 'low' 'medium' 'high' for example which would cut out the custom parsing logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is the different attributes have different types, and selecting which function overload or template instantiation to call is a compile time thing. It's not compatible with a runtime loop. Trying to use a table would require some kind of compile time iteration.

One of the things I was thinking about was being able to map an enum type to the lookup table used to map strings to enum values. It is possible to use templates to map types to values:
https://gpfault.net/posts/mapping-types-to-values.txt.html

I was thinking if we used such a solution, we could update the stringToEnum function to not require the first parameter:

template<typename T>
T stringToEnum(const std::unordered_map<std::string, T>& table, std::string value)

Instead, the table could be looked up internally using such a mapping template on the template parameter.

Taking that idea further, it could be combined with the parseElementValue function:

template<typename T, std::enable_if_t<!std::is_enum<T>::value, bool> = true>
void parseElementValue(T& destination, const NAS2D::Xml::XmlElement* element)

You could potentially remove the std::enable_if guard, and instead use a if constexpr to select whether to call parseElementValue or stringTo. And you might as well, since there would no longer be an external table parameter to pass in, so it would be very easy to call in such a context.

Would then make it easy to use a return value, rather than an out parameter.

Of course, that all hinges on being able to map a type to a value. I gave it a bit of a try, though if you define the type mapping in a different namespace than it's used, that seems to cause issues.


// Specialize this for each EnumType with a lookup table (not shown)
template <typename EnumType>
struct EnumValueName { static const std::unordered_map<std::string, EnumType> table; };

template<typename EnumType>
T stringToEnum(std::string value)
{
    const auto& table = EnumValueName<EnumType>::table;
    // Old implementation ...
}

template<typename T>
T parseElementValue(const NAS2D::Xml::XmlElement* element)
{
    try 
    {
        const auto value = element->getText();
        if constexpr (std::is_enum_v<T>)
        {
            return stringToEnum<T>(value);
        }
        else
        {
            return NAS2D::stringTo<T>(value);
        }
    }
    // Old implementation ...
}

else if (element->value() == "Hostility")
{
parseElementValue(attributes.hostility, element);
}
else if (element->value() == "MaxDepth")
{
::parseElementValue(attributes.maxDepth, element);
}
else if (element->value() == "MaxMines")
{
::parseElementValue(attributes.maxMines, element);
}
else if (element->value() == "MapImagePath")
{
::parseElementValue(attributes.mapImagePath, element);
}
else if (element->value() == "TilesetPath")
{
::parseElementValue(attributes.tilesetPath, element);
}
else if (element->value() == "Name")
{
::parseElementValue(attributes.name, element);
}
else if (element->value() == "MeanSolarDistance")
{
::parseElementValue(attributes.meanSolarDistance, element);
}
}

return attributes;
}
}
3 changes: 3 additions & 0 deletions OPHD/States/Planet.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <cmath>
#include <string>
#include <unordered_map>


class Planet
Expand Down Expand Up @@ -91,3 +92,5 @@ class Planet

NAS2D::Timer mTimer;
};

std::vector<Planet::Attributes> parsePlanetAttributes();
16 changes: 3 additions & 13 deletions OPHD/States/PlanetSelectState.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#include "PlanetSelectState.h"

#include "Planet.h"
#include "GameState.h"
#include "MapViewState.h"
#include "MainMenuState.h"

#include "../Constants.h"
#include "../Cache.h"
#include "../XmlSerializer.h"

#include <NAS2D/Utility.h>
#include <NAS2D/Mixer/Mixer.h>
Expand All @@ -19,17 +19,6 @@
using namespace NAS2D;


namespace
{
std::vector<Planet::Attributes> PlanetAttributes =
{
{ Planet::Attributes{Planet::PlanetType::Mercury, "planets/planet_d.png", Planet::Hostility::High, 1, 10, "maps/merc_01", "tsets/mercury.png", "Mercury Type", 0.4f} },
{ Planet::Attributes{Planet::PlanetType::Mars, "planets/planet_c.png", Planet::Hostility::Low, 4, 30, "maps/mars_04", "tsets/mars.png", "Mars Type", 1.524f} },
{ Planet::Attributes{Planet::PlanetType::Ganymede, "planets/planet_e.png", Planet::Hostility::Medium, 2, 15, "maps/ganymede_01", "tsets/ganymede.png", "Ganymede Type", 5.2f} }
};
}


PlanetSelectState::PlanetSelectState() :
mFontBold{ fontCache.load(constants::FONT_PRIMARY_BOLD, constants::FONT_PRIMARY_MEDIUM) },
mTinyFont{ fontCache.load(constants::FONT_PRIMARY, constants::FONT_PRIMARY_NORMAL) },
Expand All @@ -40,7 +29,8 @@ PlanetSelectState::PlanetSelectState() :
mSelect{"sfx/click.ogg"},
mHover{"sfx/menu4.ogg"},
mQuit{"Main Menu"},
mReturnState{this}
mReturnState{this},
PlanetAttributes(parsePlanetAttributes())
{}


Expand Down
6 changes: 3 additions & 3 deletions OPHD/States/PlanetSelectState.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "Planet.h"
#include "../UI/UI.h"

#include <NAS2D/State.h>
Expand All @@ -13,9 +14,6 @@
#include <vector>


class Planet;


class PlanetSelectState : public NAS2D::State
{
public:
Expand Down Expand Up @@ -63,4 +61,6 @@ class PlanetSelectState : public NAS2D::State
NAS2D::Timer mTimer;

NAS2D::State* mReturnState = this;

std::vector<Planet::Attributes> PlanetAttributes;
};
25 changes: 25 additions & 0 deletions OPHD/XmlSerializer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "XmlSerializer.h"
#include <NAS2D/Utility.h>
#include <NAS2D/Filesystem.h>

using namespace NAS2D;


Xml::XmlDocument openXmlFile(std::string filename, std::string rootElementName)
{
Xml::XmlDocument xmlDocument;
xmlDocument.parse(Utility<Filesystem>::get().open(filename).raw_bytes());

if (xmlDocument.error())
{
throw std::runtime_error(filename + " has malformed XML: Row: " + std::to_string(xmlDocument.errorRow()) +
" Column: " + std::to_string(xmlDocument.errorCol()) + " : " + xmlDocument.errorDesc());
}

const auto* xmlRootElement = xmlDocument.firstChildElement(rootElementName);
if (!xmlRootElement) {
throw std::runtime_error(filename + " does not contain required root tag of <" + rootElementName + ">");
}

return xmlDocument;
DanRStevens marked this conversation as resolved.
Show resolved Hide resolved
}
Loading