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

Refactor AnimationSet loading #964

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 153 additions & 181 deletions NAS2D/Resource/AnimationSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ namespace
{
return " (Row: " + std::to_string(row) + ")";
}

AnimationSet processXml(std::string filePath, ImageCache& imageCache);
std::map<std::string, std::string> processImageSheets(const std::string& basePath, const Xml::XmlElement* element, ImageCache& imageCache);
std::map<std::string, std::vector<AnimationSet::Frame>> processActions(const std::map<std::string, std::string>& imageSheetMap, const Xml::XmlElement* element, ImageCache& imageCache);
std::vector<AnimationSet::Frame> processFrames(const std::map<std::string, std::string>& imageSheetMap, const Xml::XmlElement* element, ImageCache& imageCache);
}


Expand All @@ -55,10 +50,36 @@ bool AnimationSet::Frame::isStopFrame() const
}


AnimationSet::AnimationSet(std::string fileName) : AnimationSet{processXml(std::move(fileName), animationImageCache)}
AnimationSet::AnimationSet(const std::string& fileName) :
mFileName{fileName}
{
try {
const auto& filesystem = Utility<Filesystem>::get();
const auto basePath = filesystem.parentPath(mFileName);

Xml::XmlDocument xmlDoc{};
xmlDoc.parse(filesystem.read(mFileName).c_str());

if(xmlDoc.error())
{
throw std::runtime_error("Sprite file has malformed XML: Row: " + std::to_string(xmlDoc.errorRow()) + " Column: " + std::to_string(xmlDoc.errorCol()) + " : " + xmlDoc.errorDesc());
}
loadFromXml(*xmlDoc.rootElement());
} catch(...) {
throw;
}
}

AnimationSet::AnimationSet(const Xml::XmlElement& element)
{
try {
loadFromXml(element);
}
catch(...)
{
throw;
}
}

AnimationSet::AnimationSet(std::string fileName, std::map<std::string, std::string> imageSheetMap, std::map<std::string, std::vector<Frame>> actions) :
mFileName{std::move(fileName)},
Expand All @@ -84,200 +105,151 @@ const std::vector<AnimationSet::Frame>& AnimationSet::frames(const std::string&
return mActions.at(actionName);
}


namespace
void AnimationSet::loadFromXml(const Xml::XmlElement& element)
{

/**
* Parses a Sprite XML Definition File.
*
* \param filePath File path of the sprite XML definition file.
*/
AnimationSet processXml(std::string filePath, ImageCache& imageCache)
try
{
try
if (const auto* xml_sprite = element.firstChildElement("sprite"); xml_sprite == nullptr)
{
auto& filesystem = Utility<Filesystem>::get();
const auto basePath = filesystem.parentPath(filePath);

Xml::XmlDocument xmlDoc;
xmlDoc.parse(filesystem.read(filePath).c_str());

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

// Find the Sprite node.
const auto* xmlRootElement = xmlDoc.firstChildElement("sprite");
if (!xmlRootElement)
{
throw std::runtime_error("Sprite file does not contain required <sprite> tag");
}

// Get the Sprite version.
const auto version = xmlRootElement->attribute("version");
if (version.empty())
throw std::runtime_error("Sprite file does not contain required <sprite> tag");
}
else
{
if(const auto version = element.attribute("version"); version.empty())
{
throw std::runtime_error("Sprite file's root element does not specify a version");
}
if (version != SPRITE_VERSION)
} else
{
throw std::runtime_error("Sprite version mismatch. Expected: " + std::string{SPRITE_VERSION} + " Actual: " + versionString());
if(version != SPRITE_VERSION)
{
throw std::runtime_error("Sprite version mismatch. Expected: " + std::string{SPRITE_VERSION} + " Actual: " + versionString());
}
}

// Note:
// Here instead of going through each element and calling a processing function to handle
// it, we just iterate through all nodes to find sprite sheets. This allows us to define
// image sheets anywhere in the sprite file.
auto imageSheetMap = processImageSheets(basePath, xmlRootElement, imageCache);
auto actions = processActions(imageSheetMap, xmlRootElement, imageCache);
return {std::move(filePath), std::move(imageSheetMap), std::move(actions)};
}
catch(const std::runtime_error& error)
mImageSheetMap = [&]() //IIIL
{
throw std::runtime_error("Error parsing Sprite file: " + filePath + "\nError: " + error.what());
}
}


/**
* Iterates through all elements of a Sprite XML definition looking
* for 'imagesheet' elements and processes them.
*
* \note Since 'imagesheet' elements are processed before any other
* element in a sprite definition, these elements can appear
* anywhere in a Sprite XML definition.
*/
std::map<std::string, std::string> processImageSheets(const std::string& basePath, const Xml::XmlElement* element, ImageCache& imageCache)
{
std::map<std::string, std::string> imageSheetMap;

for (const auto* node = element->firstChildElement("imagesheet"); node; node = node->nextSiblingElement("imagesheet"))
{
const auto dictionary = attributesToDictionary(*node);
const auto id = dictionary.get("id");
const auto src = dictionary.get("src");

if (id.empty())
{
throw std::runtime_error("Sprite imagesheet definition has `id` of length zero: " + endTag(node->row()));
}

if (src.empty())
{
throw std::runtime_error("Sprite imagesheet definition has `src` of length zero: " + endTag(node->row()));
}
std::map<std::string, std::string> imageSheetMap;

if (imageSheetMap.find(id) != imageSheetMap.end())
for (const auto* node = element.firstChildElement("imagesheet"); node != nullptr; node = node->nextSiblingElement("imagesheet"))
{
throw std::runtime_error("Sprite image sheet redefinition: id: '" + id + "' " + endTag(node->row()));
}

const auto imagePath = basePath + src;
imageSheetMap.try_emplace(id, imagePath);
imageCache.load(imagePath);
}

return imageSheetMap;
}
const auto dictionary = attributesToDictionary(*node);
const auto id = dictionary.get("id");
const auto src = dictionary.get("src");

if (id.empty())
{
throw std::runtime_error("Sprite imagesheet definition has `id` of length zero: " + endTag(node->row()));
}

/**
* Iterates through all elements of a Sprite XML definition looking
* for 'action' elements and processes them.
*/
std::map<std::string, std::vector<AnimationSet::Frame>> processActions(const std::map<std::string, std::string>& imageSheetMap, const Xml::XmlElement* element, ImageCache& imageCache)
{
std::map<std::string, std::vector<AnimationSet::Frame>> actions;

for (const auto* action = element->firstChildElement("action"); action; action = action->nextSiblingElement("action"))
{
const auto dictionary = attributesToDictionary(*action);
const auto actionName = dictionary.get("name");
if (src.empty())
{
throw std::runtime_error("Sprite imagesheet definition has `src` of length zero: " + endTag(node->row()));
}

if (actionName.empty())
{
throw std::runtime_error("Sprite Action definition has 'name' of length zero: " + endTag(action->row()));
}
if (actions.find(actionName) != actions.end())
{
throw std::runtime_error("Sprite Action redefinition: '" + actionName + "' " + endTag(action->row()));
}
if (imageSheetMap.find(id) != imageSheetMap.end())
{
throw std::runtime_error("Sprite image sheet redefinition: id: '" + id + "' " + endTag(node->row()));
}

actions[actionName] = processFrames(imageSheetMap, action, imageCache);
const auto& filesystem = Utility<Filesystem>::get();
const auto basePath = filesystem.parentPath(mFileName);

if (actions[actionName].empty())
{
throw std::runtime_error("Sprite Action contains no valid frames: " + actionName);
const auto path = basePath + src;
imageSheetMap.try_emplace(id, path);
animationImageCache.load(path);
}
}
return imageSheetMap;
}(); //IIIL

return actions;
mActions = [&]() //IIIL
{
std::map<std::string, std::vector<AnimationSet::Frame>> actions;

for (const auto* action = element.firstChildElement("action"); action != nullptr; action = action->nextSiblingElement("action"))
{
const auto dictionary = attributesToDictionary(*action);
const auto actionName = dictionary.get("name");

if (actionName.empty())
{
throw std::runtime_error("Sprite Action definition has 'name' of length zero: " + endTag(action->row()));
}
if (actions.find(actionName) != actions.end())
{
throw std::runtime_error("Sprite Action redefinition: '" + actionName + "' " + endTag(action->row()));
}

actions[actionName] = [&]() //IIIL
{
std::vector<AnimationSet::Frame> frameList;

for (const auto* frame = element.firstChildElement("frame"); frame; frame = frame->nextSiblingElement("frame"))
{
const auto currentRow = frame->row();

const auto dictionary = attributesToDictionary(*frame);
reportMissingOrUnexpected(dictionary.keys(), {"sheetid", "x", "y", "width", "height", "anchorx", "anchory"}, {"delay"});

const auto sheetId = dictionary.get("sheetid");
const auto delay = dictionary.get<unsigned int>("delay", 0);
const auto x = dictionary.get<int>("x");
const auto y = dictionary.get<int>("y");
const auto width = dictionary.get<int>("width");
const auto height = dictionary.get<int>("height");
const auto anchorx = dictionary.get<int>("anchorx");
const auto anchory = dictionary.get<int>("anchory");

if (sheetId.empty())
{
throw std::runtime_error("Sprite Frame definition has 'sheetid' of length zero: " + endTag(currentRow));
}
if (const auto iterator = mImageSheetMap.find(sheetId); iterator != mImageSheetMap.end())
{
const auto& image = animationImageCache.load(iterator->second);
// X-Coordinate
if (x < 0 || x > image.size().x)
{
throw std::runtime_error("Sprite frame attribute 'x' is out of bounds: " + endTag(currentRow));
}
// Y-Coordinate
if (y < 0 || y > image.size().y)
{
throw std::runtime_error("Sprite frame attribute 'y' is out of bounds: " + endTag(currentRow));
}
// Width
if (width <= 0 || width > image.size().x - x)
{
throw std::runtime_error("Sprite frame attribute 'width' is out of bounds: " + endTag(currentRow));
}
// Height
if (height <= 0 || height > image.size().y - y)
{
throw std::runtime_error("Sprite frame attribute 'height' is out of bounds: " + endTag(currentRow));
}

const auto bounds = Rectangle<int>::Create(Point<int>{x, y}, Vector{width, height});
const auto anchorOffset = Vector{anchorx, anchory};
frameList.emplace_back(AnimationSet::Frame{image, bounds, anchorOffset, delay});
}
else
{
throw std::runtime_error("Sprite Frame definition references undefined imagesheet: '" + sheetId + "' " + endTag(currentRow));
}
}

return frameList;
}(); //IIIL

if (actions[actionName].empty())
{
throw std::runtime_error("Sprite Action contains no valid frames: " + actionName);
}
}
return actions;
}(); //IIIL
}


/**
* Parses through all <frame> tags within an <action> tag in a Sprite Definition.
*/
std::vector<AnimationSet::Frame> processFrames(const std::map<std::string, std::string>& imageSheetMap, const Xml::XmlElement* element, ImageCache& imageCache)
catch (...)
{
std::vector<AnimationSet::Frame> frameList;

for (const auto* frame = element->firstChildElement("frame"); frame; frame = frame->nextSiblingElement("frame"))
{
int currentRow = frame->row();

const auto dictionary = attributesToDictionary(*frame);
reportMissingOrUnexpected(dictionary.keys(), {"sheetid", "x", "y", "width", "height", "anchorx", "anchory"}, {"delay"});

const auto sheetId = dictionary.get("sheetid");
const auto delay = dictionary.get<unsigned int>("delay", 0);
const auto x = dictionary.get<int>("x");
const auto y = dictionary.get<int>("y");
const auto width = dictionary.get<int>("width");
const auto height = dictionary.get<int>("height");
const auto anchorx = dictionary.get<int>("anchorx");
const auto anchory = dictionary.get<int>("anchory");

if (sheetId.empty())
{
throw std::runtime_error("Sprite Frame definition has 'sheetid' of length zero: " + endTag(currentRow));
}
const auto iterator = imageSheetMap.find(sheetId);
if (iterator == imageSheetMap.end())
{
throw std::runtime_error("Sprite Frame definition references undefined imagesheet: '" + sheetId + "' " + endTag(currentRow));
}

const auto& image = imageCache.load(iterator->second);
// X-Coordinate
if (x < 0 || x > image.size().x)
{
throw std::runtime_error("Sprite frame attribute 'x' is out of bounds: " + endTag(currentRow));
}
// Y-Coordinate
if (y < 0 || y > image.size().y)
{
throw std::runtime_error("Sprite frame attribute 'y' is out of bounds: " + endTag(currentRow));
}
// Width
if (width <= 0 || width > image.size().x - x)
{
throw std::runtime_error("Sprite frame attribute 'width' is out of bounds: " + endTag(currentRow));
}
// Height
if (height <= 0 || height > image.size().y - y)
{
throw std::runtime_error("Sprite frame attribute 'height' is out of bounds: " + endTag(currentRow));
}

const auto bounds = Rectangle<int>::Create(Point<int>{x, y}, Vector{width, height});
const auto anchorOffset = Vector{anchorx, anchory};
frameList.push_back(AnimationSet::Frame{image, bounds, anchorOffset, delay});
}

return frameList;
throw;
}

}
Loading