2024-04-04 21:39:01 +02:00
|
|
|
/*
|
|
|
|
* ObstacleSetHandler.cpp, part of VCMI engine
|
|
|
|
*
|
|
|
|
* Authors: listed in file AUTHORS in main folder
|
|
|
|
*
|
|
|
|
* License: GNU General Public License v2.0 or later
|
|
|
|
* Full text of license available in license.txt file, in main folder
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "StdInc.h"
|
|
|
|
#include "ObstacleSetHandler.h"
|
|
|
|
|
2024-04-08 12:50:41 +02:00
|
|
|
#include "../modding/IdentifierStorage.h"
|
2024-04-11 18:38:50 +02:00
|
|
|
#include "../constants/StringConstants.h"
|
2024-04-12 14:53:07 +02:00
|
|
|
#include "../TerrainHandler.h"
|
2024-04-08 12:50:41 +02:00
|
|
|
|
2024-04-04 21:39:01 +02:00
|
|
|
VCMI_LIB_NAMESPACE_BEGIN
|
|
|
|
|
2024-04-08 12:50:41 +02:00
|
|
|
ObstacleSet::ObstacleSet():
|
|
|
|
type(INVALID),
|
2024-04-10 09:40:12 +02:00
|
|
|
allowedTerrains({TerrainId::NONE})
|
2024-04-08 12:50:41 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2024-04-04 21:39:01 +02:00
|
|
|
ObstacleSet::ObstacleSet(EObstacleType type, TerrainId terrain):
|
|
|
|
type(type),
|
2024-04-10 09:40:12 +02:00
|
|
|
allowedTerrains({terrain})
|
2024-04-04 21:39:01 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void ObstacleSet::addObstacle(std::shared_ptr<const ObjectTemplate> obstacle)
|
|
|
|
{
|
|
|
|
obstacles.push_back(obstacle);
|
|
|
|
}
|
|
|
|
|
2024-04-12 10:25:13 +02:00
|
|
|
ObstacleSetFilter::ObstacleSetFilter(std::vector<ObstacleSet::EObstacleType> allowedTypes, TerrainId terrain = TerrainId::ANY_TERRAIN, FactionID faction = FactionID::ANY, EAlignment alignment = EAlignment::ANY):
|
2024-04-04 21:39:01 +02:00
|
|
|
allowedTypes(allowedTypes),
|
2024-04-11 18:10:14 +02:00
|
|
|
terrain(terrain),
|
2024-04-12 10:25:13 +02:00
|
|
|
faction(faction),
|
2024-04-11 18:10:14 +02:00
|
|
|
alignment(alignment)
|
2024-04-04 21:39:01 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2024-04-12 10:25:13 +02:00
|
|
|
ObstacleSetFilter::ObstacleSetFilter(ObstacleSet::EObstacleType allowedType, TerrainId terrain = TerrainId::ANY_TERRAIN, FactionID faction = FactionID::ANY, EAlignment alignment = EAlignment::ANY):
|
2024-04-04 21:39:01 +02:00
|
|
|
allowedTypes({allowedType}),
|
2024-04-11 18:10:14 +02:00
|
|
|
terrain(terrain),
|
2024-04-12 10:25:13 +02:00
|
|
|
faction(faction),
|
2024-04-11 18:10:14 +02:00
|
|
|
alignment(alignment)
|
2024-04-04 21:39:01 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ObstacleSetFilter::filter(const ObstacleSet &set) const
|
|
|
|
{
|
2024-04-11 18:10:14 +02:00
|
|
|
if (terrain != TerrainId::ANY_TERRAIN && !vstd::contains(set.getTerrains(), terrain))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-04-12 10:25:13 +02:00
|
|
|
if (faction != FactionID::ANY)
|
|
|
|
{
|
|
|
|
auto factions = set.getFactions();
|
|
|
|
if (!factions.empty() && !vstd::contains(factions, faction))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-11 18:10:14 +02:00
|
|
|
// TODO: Also check specific factions
|
2024-04-12 09:38:45 +02:00
|
|
|
if (alignment != EAlignment::ANY)
|
2024-04-11 18:10:14 +02:00
|
|
|
{
|
2024-04-12 09:38:45 +02:00
|
|
|
auto alignments = set.getAlignments();
|
|
|
|
if (!alignments.empty() && !vstd::contains(alignments, alignment))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2024-04-11 18:10:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
TerrainId ObstacleSetFilter::getTerrain() const
|
|
|
|
{
|
|
|
|
return terrain;
|
2024-04-04 21:39:01 +02:00
|
|
|
}
|
|
|
|
|
2024-04-10 09:40:12 +02:00
|
|
|
std::set<TerrainId> ObstacleSet::getTerrains() const
|
2024-04-04 21:39:01 +02:00
|
|
|
{
|
2024-04-10 09:40:12 +02:00
|
|
|
return allowedTerrains;
|
2024-04-04 21:39:01 +02:00
|
|
|
}
|
|
|
|
|
2024-04-10 09:40:12 +02:00
|
|
|
void ObstacleSet::setTerrain(TerrainId terrain)
|
2024-04-04 21:39:01 +02:00
|
|
|
{
|
2024-04-10 09:40:12 +02:00
|
|
|
this->allowedTerrains = {terrain};
|
2024-04-04 21:39:01 +02:00
|
|
|
}
|
|
|
|
|
2024-04-10 09:40:12 +02:00
|
|
|
void ObstacleSet::setTerrains(const std::set<TerrainId> & terrains)
|
2024-04-08 12:50:41 +02:00
|
|
|
{
|
2024-04-10 09:40:12 +02:00
|
|
|
this->allowedTerrains = terrains;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ObstacleSet::addTerrain(TerrainId terrain)
|
|
|
|
{
|
|
|
|
this->allowedTerrains.insert(terrain);
|
2024-04-08 12:50:41 +02:00
|
|
|
}
|
|
|
|
|
2024-04-12 10:25:13 +02:00
|
|
|
std::set<FactionID> ObstacleSet::getFactions() const
|
|
|
|
{
|
|
|
|
return allowedFactions;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ObstacleSet::addFaction(FactionID faction)
|
|
|
|
{
|
|
|
|
this->allowedFactions.insert(faction);
|
|
|
|
}
|
|
|
|
|
2024-04-11 18:10:14 +02:00
|
|
|
void ObstacleSet::addAlignment(EAlignment alignment)
|
|
|
|
{
|
|
|
|
this->allowedAlignments.insert(alignment);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::set<EAlignment> ObstacleSet::getAlignments() const
|
|
|
|
{
|
|
|
|
return allowedAlignments;
|
|
|
|
}
|
|
|
|
|
2024-04-04 21:39:01 +02:00
|
|
|
ObstacleSet::EObstacleType ObstacleSet::getType() const
|
|
|
|
{
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
|
2024-04-08 12:50:41 +02:00
|
|
|
void ObstacleSet::setType(EObstacleType type)
|
|
|
|
{
|
|
|
|
this->type = type;
|
|
|
|
}
|
|
|
|
|
2024-04-04 21:39:01 +02:00
|
|
|
std::vector<std::shared_ptr<const ObjectTemplate>> ObstacleSet::getObstacles() const
|
|
|
|
{
|
|
|
|
return obstacles;
|
|
|
|
}
|
|
|
|
|
|
|
|
ObstacleSet::EObstacleType ObstacleSetHandler::convertObstacleClass(MapObjectID id)
|
|
|
|
{
|
|
|
|
switch (id)
|
|
|
|
{
|
|
|
|
case Obj::MOUNTAIN:
|
|
|
|
case Obj::VOLCANIC_VENT:
|
|
|
|
case Obj::VOLCANO:
|
|
|
|
case Obj::REEF:
|
|
|
|
return ObstacleSet::MOUNTAINS;
|
|
|
|
case Obj::OAK_TREES:
|
|
|
|
case Obj::PINE_TREES:
|
|
|
|
case Obj::TREES:
|
|
|
|
case Obj::DEAD_VEGETATION:
|
|
|
|
case Obj::HEDGE:
|
|
|
|
case Obj::KELP:
|
|
|
|
case Obj::WILLOW_TREES:
|
|
|
|
case Obj::YUCCA_TREES:
|
|
|
|
return ObstacleSet::TREES;
|
|
|
|
case Obj::FROZEN_LAKE:
|
|
|
|
case Obj::LAKE:
|
|
|
|
case Obj::LAVA_FLOW:
|
|
|
|
case Obj::LAVA_LAKE:
|
|
|
|
return ObstacleSet::LAKES;
|
|
|
|
case Obj::CANYON:
|
|
|
|
case Obj::CRATER:
|
|
|
|
case Obj::SAND_PIT:
|
|
|
|
case Obj::TAR_PIT:
|
|
|
|
return ObstacleSet::CRATERS;
|
|
|
|
case Obj::HILL:
|
|
|
|
case Obj::MOUND:
|
|
|
|
case Obj::OUTCROPPING:
|
|
|
|
case Obj::ROCK:
|
2024-04-05 08:31:05 +02:00
|
|
|
case Obj::SAND_DUNE:
|
2024-04-04 21:39:01 +02:00
|
|
|
case Obj::STALAGMITE:
|
|
|
|
return ObstacleSet::ROCKS;
|
|
|
|
case Obj::BUSH:
|
|
|
|
case Obj::CACTUS:
|
|
|
|
case Obj::FLOWERS:
|
|
|
|
case Obj::MUSHROOMS:
|
|
|
|
case Obj::LOG:
|
|
|
|
case Obj::MANDRAKE:
|
|
|
|
case Obj::MOSS:
|
|
|
|
case Obj::PLANT:
|
|
|
|
case Obj::SHRUB:
|
|
|
|
case Obj::STUMP:
|
|
|
|
case Obj::VINE:
|
|
|
|
return ObstacleSet::PLANTS;
|
|
|
|
case Obj::SKULL:
|
|
|
|
return ObstacleSet::ANIMALS;
|
|
|
|
default:
|
|
|
|
return ObstacleSet::OTHER;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-05 08:31:05 +02:00
|
|
|
ObstacleSet::EObstacleType ObstacleSet::typeFromString(const std::string &str)
|
|
|
|
{
|
|
|
|
static const std::map<std::string, EObstacleType> OBSTACLE_TYPE_NAMES =
|
|
|
|
{
|
|
|
|
{"mountain", MOUNTAINS},
|
|
|
|
{"tree", TREES},
|
|
|
|
{"lake", LAKES},
|
|
|
|
{"crater", CRATERS},
|
|
|
|
{"rock", ROCKS},
|
|
|
|
{"plant", PLANTS},
|
|
|
|
{"structure", STRUCTURES},
|
|
|
|
{"animal", ANIMALS},
|
|
|
|
{"other", OTHER}
|
|
|
|
};
|
|
|
|
|
|
|
|
if (OBSTACLE_TYPE_NAMES.find(str) != OBSTACLE_TYPE_NAMES.end())
|
|
|
|
{
|
|
|
|
return OBSTACLE_TYPE_NAMES.at(str);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: How to handle that?
|
|
|
|
throw std::runtime_error("Invalid obstacle type: " + str);
|
|
|
|
}
|
|
|
|
|
2024-04-08 12:50:41 +02:00
|
|
|
std::string ObstacleSet::toString() const
|
|
|
|
{
|
|
|
|
static const std::map<EObstacleType, std::string> OBSTACLE_TYPE_STRINGS =
|
|
|
|
{
|
|
|
|
{MOUNTAINS, "mountain"},
|
|
|
|
{TREES, "tree"},
|
|
|
|
{LAKES, "lake"},
|
|
|
|
{CRATERS, "crater"},
|
|
|
|
{ROCKS, "rock"},
|
|
|
|
{PLANTS, "plant"},
|
|
|
|
{STRUCTURES, "structure"},
|
|
|
|
{ANIMALS, "animal"},
|
|
|
|
{OTHER, "other"}
|
|
|
|
};
|
|
|
|
|
|
|
|
return OBSTACLE_TYPE_STRINGS.at(type);
|
|
|
|
}
|
|
|
|
|
2024-04-04 21:39:01 +02:00
|
|
|
std::vector<ObstacleSet::EObstacleType> ObstacleSetFilter::getAllowedTypes() const
|
|
|
|
{
|
|
|
|
return allowedTypes;
|
|
|
|
}
|
|
|
|
|
2024-04-11 18:10:14 +02:00
|
|
|
void ObstacleSetFilter::setType(ObstacleSet::EObstacleType type)
|
|
|
|
{
|
|
|
|
allowedTypes = {type};
|
|
|
|
}
|
|
|
|
|
|
|
|
void ObstacleSetFilter::setTypes(std::vector<ObstacleSet::EObstacleType> types)
|
|
|
|
{
|
|
|
|
this->allowedTypes = types;
|
|
|
|
}
|
|
|
|
|
2024-04-08 12:50:41 +02:00
|
|
|
std::vector<JsonNode> ObstacleSetHandler::loadLegacyData()
|
|
|
|
{
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
void ObstacleSetHandler::loadObject(std::string scope, std::string name, const JsonNode & data)
|
|
|
|
{
|
|
|
|
auto os = loadFromJson(scope, data, name, biomes.size());
|
|
|
|
if(os)
|
|
|
|
{
|
|
|
|
addObstacleSet(os);
|
2024-04-12 06:29:01 +02:00
|
|
|
// TODO: Define some const array of object types ("biome" etc.)
|
|
|
|
VLC->identifiersHandler->registerObject(scope, "biome", name, biomes.back()->id);
|
2024-04-08 12:50:41 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
logMod->error("Failed to load obstacle set: %s", name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ObstacleSetHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index)
|
|
|
|
{
|
|
|
|
auto os = loadFromJson(scope, data, name, index);
|
|
|
|
if(os)
|
|
|
|
{
|
|
|
|
addObstacleSet(os);
|
2024-04-12 06:29:01 +02:00
|
|
|
VLC->identifiersHandler->registerObject(scope, "biome", name, biomes.at(index)->id);
|
2024-04-08 12:50:41 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
logMod->error("Failed to load obstacle set: %s", name);
|
|
|
|
}
|
2024-04-12 06:29:01 +02:00
|
|
|
|
2024-04-08 12:50:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
std::shared_ptr<ObstacleSet> ObstacleSetHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index)
|
|
|
|
{
|
|
|
|
auto os = std::make_shared<ObstacleSet>();
|
2024-04-12 06:29:01 +02:00
|
|
|
os->id = index;
|
2024-04-08 12:50:41 +02:00
|
|
|
|
|
|
|
auto biome = json["biome"].Struct();
|
|
|
|
os->setType(ObstacleSet::typeFromString(biome["objectType"].String()));
|
|
|
|
|
2024-04-12 09:38:45 +02:00
|
|
|
// TODO: Handle any (every) terrain option
|
|
|
|
|
2024-04-10 09:40:12 +02:00
|
|
|
if (biome["terrain"].isString())
|
|
|
|
{
|
|
|
|
auto terrainName = biome["terrain"].String();
|
2024-04-08 12:50:41 +02:00
|
|
|
|
2024-04-10 09:40:12 +02:00
|
|
|
VLC->identifiers()->requestIdentifier(scope, "terrain", terrainName, [os](si32 id)
|
|
|
|
{
|
|
|
|
os->setTerrain(TerrainId(id));
|
|
|
|
});
|
|
|
|
}
|
2024-04-12 14:53:07 +02:00
|
|
|
else if (biome["terrain"].isVector())
|
2024-04-08 12:50:41 +02:00
|
|
|
{
|
2024-04-10 09:40:12 +02:00
|
|
|
auto terrains = biome["terrain"].Vector();
|
|
|
|
|
|
|
|
for (const auto & terrain : terrains)
|
|
|
|
{
|
|
|
|
VLC->identifiers()->requestIdentifier(scope, "terrain", terrain.String(), [os](si32 id)
|
|
|
|
{
|
|
|
|
os->addTerrain(TerrainId(id));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2024-04-12 14:53:07 +02:00
|
|
|
else
|
|
|
|
{
|
|
|
|
logMod->error("No terrain specified for obstacle set %s", name);
|
|
|
|
}
|
2024-04-08 12:50:41 +02:00
|
|
|
|
2024-04-12 14:53:07 +02:00
|
|
|
auto handleFaction = [os, scope](const std::string & str)
|
2024-04-12 10:25:13 +02:00
|
|
|
{
|
|
|
|
VLC->identifiers()->requestIdentifier(scope, "faction", str, [os](si32 id)
|
|
|
|
{
|
|
|
|
os->addFaction(FactionID(id));
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
if (biome["faction"].isString())
|
|
|
|
{
|
|
|
|
auto factionName = biome["faction"].String();
|
2024-04-12 14:53:07 +02:00
|
|
|
handleFaction(factionName);
|
2024-04-12 10:25:13 +02:00
|
|
|
}
|
|
|
|
else if (biome["faction"].isVector())
|
|
|
|
{
|
|
|
|
auto factions = biome["faction"].Vector();
|
|
|
|
for (const auto & node : factions)
|
|
|
|
{
|
2024-04-12 14:53:07 +02:00
|
|
|
handleFaction(node.String());
|
2024-04-12 10:25:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-11 18:10:14 +02:00
|
|
|
// TODO: Move this parser to some utils
|
|
|
|
auto parseAlignment = [](const std::string & str) ->EAlignment
|
|
|
|
{
|
|
|
|
int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, str);
|
|
|
|
if (alignment == -1)
|
2024-04-11 18:38:50 +02:00
|
|
|
{
|
2024-04-11 18:10:14 +02:00
|
|
|
logGlobal->error("Incorrect alignment: ", str);
|
2024-04-11 18:38:50 +02:00
|
|
|
return EAlignment::ANY;
|
|
|
|
}
|
2024-04-11 18:10:14 +02:00
|
|
|
else
|
2024-04-11 18:38:50 +02:00
|
|
|
{
|
2024-04-11 18:10:14 +02:00
|
|
|
return static_cast<EAlignment>(alignment);
|
2024-04-11 18:38:50 +02:00
|
|
|
}
|
2024-04-11 18:10:14 +02:00
|
|
|
};
|
|
|
|
|
2024-04-12 09:38:45 +02:00
|
|
|
if (biome["alignment"].isString())
|
2024-04-11 18:10:14 +02:00
|
|
|
{
|
2024-04-12 09:38:45 +02:00
|
|
|
os->addAlignment(parseAlignment(biome["alignment"].String()));
|
2024-04-11 18:10:14 +02:00
|
|
|
}
|
2024-04-12 09:38:45 +02:00
|
|
|
else if (biome["alignment"].isVector())
|
2024-04-11 18:10:14 +02:00
|
|
|
{
|
2024-04-12 09:38:45 +02:00
|
|
|
auto alignments = biome["alignment"].Vector();
|
2024-04-11 18:10:14 +02:00
|
|
|
for (const auto & node : alignments)
|
|
|
|
{
|
|
|
|
os->addAlignment(parseAlignment(node.String()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 12:50:41 +02:00
|
|
|
auto templates = json["templates"].Vector();
|
|
|
|
for (const auto & node : templates)
|
|
|
|
{
|
2024-04-09 17:29:33 +02:00
|
|
|
logGlobal->info("Registering obstacle template: %s in scope %s", node.String(), scope);
|
2024-04-08 12:50:41 +02:00
|
|
|
|
2024-04-09 17:29:33 +02:00
|
|
|
auto identifier = boost::algorithm::to_lower_copy(node.String());
|
2024-04-09 22:09:03 +02:00
|
|
|
auto jsonName = JsonNode(identifier);
|
|
|
|
|
|
|
|
VLC->identifiers()->requestIdentifier(node.getModScope(), "obstacleTemplate", identifier, [this, os](si32 id)
|
2024-04-08 12:50:41 +02:00
|
|
|
{
|
|
|
|
logGlobal->info("Resolved obstacle id: %d", id);
|
|
|
|
os->addObstacle(obstacleTemplates[id]);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return os;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ObstacleSetHandler::addTemplate(const std::string & scope, const std::string &name, std::shared_ptr<const ObjectTemplate> tmpl)
|
|
|
|
{
|
|
|
|
auto id = obstacleTemplates.size();
|
|
|
|
|
2024-04-09 17:29:33 +02:00
|
|
|
auto strippedName = boost::algorithm::to_lower_copy(name);
|
2024-04-08 12:50:41 +02:00
|
|
|
auto pos = strippedName.find(".def");
|
|
|
|
if(pos != std::string::npos)
|
|
|
|
strippedName.erase(pos, 4);
|
|
|
|
|
2024-04-09 17:29:33 +02:00
|
|
|
if (VLC->identifiersHandler->getIdentifier(scope, "obstacleTemplate", strippedName, true))
|
|
|
|
{
|
|
|
|
logMod->warn("Duplicate obstacle template: %s", strippedName);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Save by name
|
|
|
|
VLC->identifiersHandler->registerObject(scope, "obstacleTemplate", strippedName, id);
|
2024-04-08 12:50:41 +02:00
|
|
|
|
2024-04-09 17:29:33 +02:00
|
|
|
// Index by id
|
|
|
|
obstacleTemplates[id] = tmpl;
|
|
|
|
}
|
2024-04-08 12:50:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void ObstacleSetHandler::addObstacleSet(std::shared_ptr<ObstacleSet> os)
|
2024-04-04 21:39:01 +02:00
|
|
|
{
|
2024-04-08 12:50:41 +02:00
|
|
|
obstacleSets[os->getType()].push_back(os);
|
|
|
|
biomes.push_back(os);
|
2024-04-04 21:39:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
TObstacleTypes ObstacleSetHandler::getObstacles( const ObstacleSetFilter &filter) const
|
|
|
|
{
|
|
|
|
TObstacleTypes result;
|
|
|
|
|
|
|
|
for (const auto &allowedType : filter.getAllowedTypes())
|
|
|
|
{
|
|
|
|
auto it = obstacleSets.find(allowedType);
|
|
|
|
if(it != obstacleSets.end())
|
|
|
|
{
|
|
|
|
for (const auto &os : it->second)
|
|
|
|
{
|
2024-04-08 12:50:41 +02:00
|
|
|
if (filter.filter(*os))
|
2024-04-04 21:39:01 +02:00
|
|
|
{
|
|
|
|
result.push_back(os);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
VCMI_LIB_NAMESPACE_END
|
|
|
|
|