/*
* The ManaPlus Client
* Copyright (C) 2004-2009 The Mana World Development Team
* Copyright (C) 2009-2010 The Mana Developers
* Copyright (C) 2011-2020 The ManaPlus Developers
* Copyright (C) 2020-2023 The ManaVerse Developers
*
* This file is part of The ManaPlus Client.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "resources/sprite/spritedef.h"
#include "configuration.h"
#include "settings.h"
#include "const/resources/spriteaction.h"
#include "const/resources/map/map.h"
#include "utils/checkutils.h"
#include "utils/foreach.h"
#include "resources/action.h"
#include "resources/imageset.h"
#include "resources/animation/animation.h"
#include "resources/dye/dye.h"
#include "resources/loaders/imagesetloader.h"
#include "resources/loaders/xmlloader.h"
#include "resources/sprite/spritereference.h"
#include "debug.h"
SpriteReference *SpriteReference::Empty = nullptr;
const Action *SpriteDef::getAction(const std::string &action,
const unsigned num) const
{
Actions::const_iterator i = mActions.find(num);
if (i == mActions.end() && num != 100)
i = mActions.find(100);
if (i == mActions.end() || ((*i).second == nullptr))
return nullptr;
const ActionMap *const actMap = (*i).second;
if (actMap == nullptr)
return nullptr;
const ActionMap::const_iterator it = actMap->find(action);
if (it == actMap->end())
{
logger->log("Warning: no action \"%s\" defined!", action.c_str());
return nullptr;
}
return (*it).second;
}
unsigned SpriteDef::findNumber(const unsigned num) const
{
unsigned min = 101;
FOR_EACH (Actions::const_iterator, it, mActions)
{
const unsigned n = (*it).first;
if (n >= num && n < min)
min = n;
}
if (min == 101)
return 0;
return min;
}
SpriteDef *SpriteDef::load(const std::string &animationFile,
const int variant, const bool prot)
{
BLOCK_START("SpriteDef::load")
const size_t pos = animationFile.find('|');
std::string palettes;
if (pos != std::string::npos)
palettes = animationFile.substr(pos + 1);
XML::Document *const doc = Loader::getXml(animationFile.substr(0, pos),
UseVirtFs_true,
SkipError_false);
if (doc == nullptr)
return nullptr;
XmlNodePtrConst rootNode = doc->rootNode();
if ((rootNode == nullptr) || !xmlNameEqual(rootNode, "sprite"))
{
reportAlways("Error, failed to parse sprite %s",
animationFile.c_str())
const std::string errorFile = pathJoin(paths.getStringValue("sprites"),
paths.getStringValue("spriteErrorFile"));
BLOCK_END("SpriteDef::load")
doc->decRef();
if (animationFile != errorFile)
return load(errorFile, 0, prot);
return nullptr;
}
SpriteDef *const def = new SpriteDef;
def->mSource = animationFile;
def->mProcessedFiles.insert(animationFile);
def->loadSprite(rootNode, variant, palettes);
def->substituteActions();
if (settings.fixDeadAnimation)
def->fixDeadAction();
if (prot)
{
def->incRef();
def->mProtected = true;
}
doc->decRef();
BLOCK_END("SpriteDef::load")
return def;
}
void SpriteDef::fixDeadAction()
{
FOR_EACH (ActionsIter, it, mActions)
{
ActionMap *const d = (*it).second;
if (d == nullptr)
continue;
const ActionMap::iterator i = d->find(SpriteAction::DEAD);
const ActionMap::iterator i2 = d->find(SpriteAction::STAND);
// search dead action and check what it not same with stand action
if (i != d->end() &&
i->second != nullptr &&
(i2 == d->end() || i->second != i2->second))
{
(i->second)->setLastFrameDelay(0);
}
}
}
void SpriteDef::substituteAction(const std::string &restrict complete,
const std::string &restrict with)
{
FOR_EACH (ActionsConstIter, it, mActions)
{
ActionMap *const d = (*it).second;
if (reportTrue(d == nullptr))
continue;
if (d->find(complete) == d->end())
{
const ActionMap::iterator i = d->find(with);
if (i != d->end())
(*d)[complete] = i->second;
}
}
}
void SpriteDef::substituteActions()
{
substituteAction(SpriteAction::STAND, SpriteAction::DEFAULT);
substituteAction(SpriteAction::MOVE, SpriteAction::STAND);
substituteAction(SpriteAction::ATTACK, SpriteAction::STAND);
substituteAction(SpriteAction::CAST, SpriteAction::ATTACK);
substituteAction(SpriteAction::SIT, SpriteAction::STAND);
substituteAction(SpriteAction::SITTOP, SpriteAction::SIT);
substituteAction(SpriteAction::DEAD, SpriteAction::STAND);
substituteAction(SpriteAction::SPAWN, SpriteAction::STAND);
substituteAction(SpriteAction::FLY, SpriteAction::MOVE);
substituteAction(SpriteAction::SWIM, SpriteAction::MOVE);
substituteAction(SpriteAction::RIDE, SpriteAction::MOVE);
substituteAction(SpriteAction::STANDSKY, SpriteAction::STAND);
substituteAction(SpriteAction::STANDWATER, SpriteAction::STAND);
substituteAction(SpriteAction::STANDRIDE, SpriteAction::STAND);
substituteAction(SpriteAction::SITSKY, SpriteAction::SIT);
substituteAction(SpriteAction::SITWATER, SpriteAction::SIT);
substituteAction(SpriteAction::SITRIDE, SpriteAction::SIT);
substituteAction(SpriteAction::ATTACKSKY, SpriteAction::ATTACK);
substituteAction(SpriteAction::ATTACKWATER, SpriteAction::ATTACK);
substituteAction(SpriteAction::ATTACKRIDE, SpriteAction::ATTACK);
substituteAction(SpriteAction::CASTSKY, SpriteAction::CAST);
substituteAction(SpriteAction::CASTWATER, SpriteAction::CAST);
substituteAction(SpriteAction::CASTRIDE, SpriteAction::CAST);
substituteAction(SpriteAction::SPAWNSKY, SpriteAction::SPAWN);
substituteAction(SpriteAction::SPAWNWATER, SpriteAction::SPAWN);
substituteAction(SpriteAction::SPAWNRIDE, SpriteAction::SPAWN);
substituteAction(SpriteAction::DEADSKY, SpriteAction::DEAD);
substituteAction(SpriteAction::DEADWATER, SpriteAction::DEAD);
substituteAction(SpriteAction::DEADRIDE, SpriteAction::DEAD);
}
void SpriteDef::loadSprite(XmlNodeConstPtr spriteNode,
const int variant,
const std::string &palettes)
{
BLOCK_START("SpriteDef::loadSprite")
if (spriteNode == nullptr)
{
BLOCK_END("SpriteDef::loadSprite")
return;
}
// Get the variant
const int variantCount = XML::getProperty(spriteNode, "variants", 0);
int variant_offset = 0;
if (variantCount > 0 && variant < variantCount)
{
variant_offset = variant * XML::getProperty(spriteNode,
"variant_offset",
0);
}
for_each_xml_child_node(node, spriteNode)
{
if (xmlNameEqual(node, "imageset"))
loadImageSet(node, palettes);
else if (xmlNameEqual(node, "action"))
loadAction(node, variant_offset);
else if (xmlNameEqual(node, "include"))
includeSprite(node, variant);
}
BLOCK_END("SpriteDef::loadSprite")
}
void SpriteDef::loadImageSet(XmlNodeConstPtr node,
const std::string &palettes)
{
const std::string name = XML::getProperty(node, "name", "");
// We don't allow redefining image sets. This way, an included sprite
// definition will use the already loaded image set with the same name.
if (mImageSets.find(name) != mImageSets.end())
return;
const int width = XML::getProperty(node, "width", 0);
const int height = XML::getProperty(node, "height", 0);
std::string imageSrc = XML::getProperty(node, "src", "");
Dye::instantiate(imageSrc, palettes);
ImageSet *const imageSet = Loader::getImageSet(imageSrc,
width, height);
if (imageSet == nullptr)
{
reportAlways("%s: Couldn't load imageset: %s",
mSource.c_str(),
imageSrc.c_str())
return;
}
imageSet->setOffsetX(XML::getProperty(node, "offsetX", 0));
imageSet->setOffsetY(XML::getProperty(node, "offsetY", 0));
mImageSets[name] = imageSet;
}
const ImageSet *SpriteDef::getImageSet(const std::string &imageSetName) const
{
const ImageSetCIterator si = mImageSets.find(imageSetName);
if (si == mImageSets.end())
{
reportAlways("%s: Imageset \"%s\" not defined in %s",
mSource.c_str(),
imageSetName.c_str(),
mIdPath.c_str())
return nullptr;
}
return si->second;
}
void SpriteDef::loadAction(XmlNodeConstPtr node,
const int variant_offset)
{
if (node == nullptr)
return;
const std::string actionName = XML::getProperty(node, "name", "");
const std::string imageSetName = XML::getProperty(node, "imageset", "");
const unsigned hp = XML::getProperty(node, "hp", 100);
const ImageSet *const imageSet = getImageSet(imageSetName);
if (actionName == SpriteAction::INVALID)
{
reportAlways("%s: Unknown action \"%s\" defined in %s",
mSource.c_str(),
actionName.c_str(),
mIdPath.c_str())
return;
}
Action *const action = new Action(actionName);
action->setNumber(hp);
addAction(hp, actionName, action);
// dirty hack to fix bad resources in tmw server
if (actionName == "attack_stab")
{
reportAlways("Found legacy attribute attack_stab in animation")
addAction(hp, "attack", action);
}
// When first action, set it as default direction.
// i here always correct, because hp was added above.
const Actions::const_iterator i = mActions.find(hp);
if ((*i).second->size() == 1)
addAction(hp, SpriteAction::DEFAULT, action);
// Load animations
for_each_xml_child_node(animationNode, node)
{
if (xmlNameEqual(animationNode, "animation"))
loadAnimation(animationNode, action, imageSet, variant_offset);
}
}
void SpriteDef::loadAnimation(XmlNodeConstPtr animationNode,
Action *const action,
const ImageSet *const imageSet0,
const int variant_offset) const
{
if (action == nullptr ||
imageSet0 == nullptr ||
animationNode == nullptr)
{
return;
}
const std::string directionName =
XML::getProperty(animationNode, "direction", "");
const SpriteDirection::Type directionType
= makeSpriteDirection(directionName);
if (directionType == SpriteDirection::INVALID)
{
reportAlways("%s: Unknown direction \"%s\" used in %s",
mSource.c_str(),
directionName.c_str(),
mIdPath.c_str())
return;
}
Animation *const animation = new Animation(directionName);
action->setAnimation(directionType, animation);
// Get animation frames
for_each_xml_child_node(frameNode, animationNode)
{
const int delay = XML::getIntProperty(
frameNode, "delay", 0, 0, 100000);
const std::string imageSetName = XML::getProperty(frameNode,
"imageset",
"");
const ImageSet *imageSet = imageSet0;
if (!imageSetName.empty())
{
imageSet = getImageSet(imageSetName);
if (imageSet == nullptr)
imageSet = imageSet0;
}
const int offsetX = XML::getProperty(frameNode, "offsetX", 0)
+ imageSet->getOffsetX() - imageSet->getWidth() / 2
+ mapTileSize / 2;
const int offsetY = XML::getProperty(frameNode, "offsetY", 0)
+ imageSet->getOffsetY() - imageSet->getHeight() + mapTileSize;
const int rand = XML::getIntProperty(frameNode, "rand", 100, 0, 100);
if (xmlNameEqual(frameNode, "frame"))
{
const int index = XML::getProperty(frameNode, "index", -1);
if (index < 0)
{
reportAlways(
"%s: No valid value for 'index' at direction '%s'",
mSource.c_str(),
directionName.c_str())
continue;
}
Image *const img = imageSet->get(index + variant_offset);
if (img == nullptr)
{
reportAlways("%s: No image at index %d at direction '%s'",
mSource.c_str(),
index + variant_offset,
directionName.c_str())
continue;
}
animation->addFrame(img, delay, offsetX, offsetY, rand);
}
else if (xmlNameEqual(frameNode, "sequence"))
{
const int start = XML::getProperty(frameNode, "start", -1);
const int end = XML::getProperty(frameNode, "end", -1);
const std::string value = XML::getProperty(frameNode, "value", "");
const int repeat = XML::getIntProperty(
frameNode, "repeat", 1, 0, 100);
if (repeat < 1)
{
reportAlways("%s: No valid value for 'repeat' at direction %s",
mSource.c_str(),
directionName.c_str())
continue;
}
if (value.empty())
{
if (addSequence(start, end, delay, offsetX, offsetY,
variant_offset, repeat, rand, imageSet, animation))
{
continue;
}
}
else
{
StringVect vals;
splitToStringVector(vals, value, ',');
FOR_EACH (StringVectCIter, it, vals)
{
const std::string str = *it;
const size_t idx = str.find('-');
if (str == "p")
{
animation->addPause(delay, rand);
}
else if (idx != std::string::npos)
{
const int v1 = atoi(str.substr(0, idx).c_str());
const int v2 = atoi(str.substr(idx + 1).c_str());
addSequence(v1, v2, delay, offsetX, offsetY,
variant_offset, repeat, rand, imageSet, animation);
}
else
{
Image *const img = imageSet->get(atoi(
str.c_str()) + variant_offset);
if (img != nullptr)
{
animation->addFrame(img, delay,
offsetX, offsetY, rand);
}
}
}
}
}
else if (xmlNameEqual(frameNode, "pause"))
{
animation->addPause(delay, rand);
}
else if (xmlNameEqual(frameNode, "end"))
{
animation->addTerminator(rand);
}
else if (xmlNameEqual(frameNode, "jump"))
{
animation->addJump(XML::getProperty(
frameNode, "action", ""), rand);
}
else if (xmlNameEqual(frameNode, "label"))
{
const std::string name = XML::getProperty(frameNode, "name", "");
if (!name.empty())
animation->addLabel(name);
}
else if (xmlNameEqual(frameNode, "goto"))
{
const std::string name = XML::getProperty(frameNode, "label", "");
if (!name.empty())
animation->addGoto(name, rand);
}
} // for frameNode
}
void SpriteDef::includeSprite(XmlNodeConstPtr includeNode, const int variant)
{
std::string filename = XML::getProperty(includeNode, "file", "");
if (filename.empty())
return;
filename = pathJoin(paths.getStringValue("sprites"), filename);
if (mProcessedFiles.find(filename) != mProcessedFiles.end())
{
reportAlways("%s: Tried to include %s which already is included.",
mSource.c_str(),
filename.c_str())
return;
}
mProcessedFiles.insert(filename);
XML::Document *const doc = Loader::getXml(filename,
UseVirtFs_true,
SkipError_false);
if (doc == nullptr)
return;
XmlNodeConstPtr rootNode = doc->rootNode();
if ((rootNode == nullptr) || !xmlNameEqual(rootNode, "sprite"))
{
reportAlways("%s: No sprite root node in %s",
mSource.c_str(),
filename.c_str())
doc->decRef();
return;
}
loadSprite(rootNode, variant, std::string());
doc->decRef();
}
SpriteDef::~SpriteDef()
{
// Actions are shared, so ensure they are deleted only once.
std::set actions;
FOR_EACH (Actions::iterator, i, mActions)
{
FOR_EACHP (ActionMap::iterator, it, (*i).second)
actions.insert(it->second);
delete (*i).second;
}
FOR_EACH (std::set::const_iterator, i, actions)
delete *i;
mActions.clear();
FOR_EACH (ImageSetIterator, i, mImageSets)
{
if (i->second != nullptr)
{
i->second->decRef();
i->second = nullptr;
}
}
mImageSets.clear();
}
SpriteDirection::Type SpriteDef::makeSpriteDirection(const std::string
&direction)
{
if (direction.empty() || direction == "default")
return SpriteDirection::DEFAULT;
else if (direction == "up")
return SpriteDirection::UP;
else if (direction == "left")
return SpriteDirection::LEFT;
else if (direction == "right")
return SpriteDirection::RIGHT;
else if (direction == "down")
return SpriteDirection::DOWN;
else if (direction == "upleft")
return SpriteDirection::UPLEFT;
else if (direction == "upright")
return SpriteDirection::UPRIGHT;
else if (direction == "downleft")
return SpriteDirection::DOWNLEFT;
else if (direction == "downright")
return SpriteDirection::DOWNRIGHT;
else
return SpriteDirection::INVALID;
}
void SpriteDef::addAction(const unsigned hp, const std::string &name,
Action *const action)
{
const Actions::const_iterator i = mActions.find(hp);
if (i == mActions.end())
mActions[hp] = new ActionMap;
(*mActions[hp])[name] = action;
}
bool SpriteDef::addSequence(const int start,
const int end,
const int delay,
const int offsetX,
const int offsetY,
const int variant_offset,
int repeat,
const int rand,
const ImageSet *const imageSet,
Animation *const animation) const
{
if ((imageSet == nullptr) || (animation == nullptr))
return true;
if (start < 0 || end < 0)
{
reportAlways("%s: No valid value for 'start' or 'end'",
mSource.c_str())
return true;
}
if (start <= end)
{
while (repeat > 0)
{
int pos = start;
while (end >= pos)
{
Image *const img = imageSet->get(pos + variant_offset);
if (img == nullptr)
{
reportAlways("%s: No image at index %d",
mSource.c_str(),
pos + variant_offset)
pos ++;
continue;
}
animation->addFrame(img, delay,
offsetX, offsetY, rand);
pos ++;
}
repeat --;
}
}
else
{
while (repeat > 0)
{
int pos = start;
while (end <= pos)
{
Image *const img = imageSet->get(pos + variant_offset);
if (img == nullptr)
{
reportAlways("%s: No image at index %d",
mSource.c_str(),
pos + variant_offset)
pos ++;
continue;
}
animation->addFrame(img, delay,
offsetX, offsetY, rand);
pos --;
}
repeat --;
}
}
return false;
}
int SpriteDef::calcMemoryLocal() const
{
int sz = static_cast(sizeof(SpriteDef) +
sizeof(ImageSets) +
sizeof(Actions) +
sizeof(std::set)) +
Resource::calcMemoryLocal();
FOR_EACH (std::set::const_iterator, it, mProcessedFiles)
{
sz += static_cast((*it).capacity());
}
return sz;
}
int SpriteDef::calcMemoryChilds(const int level) const
{
int sz = 0;
FOR_EACH (ImageSets::const_iterator, it, mImageSets)
{
sz += static_cast((*it).first.capacity());
const ImageSet *const imageSet = (*it).second;
sz += imageSet->calcMemory(level + 1);
}
FOR_EACH (ActionsCIter, it, mActions)
{
sz += sizeof(unsigned);
const ActionMap *const actionMap = (*it).second;
FOR_EACHP (ActionMap::const_iterator, it2, actionMap)
{
sz += static_cast((*it2).first.capacity());
const Action *const action = (*it2).second;
sz += action->calcMemory(level + 1);
}
}
return sz;
}