mirror of
https://github.com/vcmi/vcmi.git
synced 2024-12-22 22:13:35 +02:00
327ff01471
Added flag `manualHeroVisit` flag to town building. If this flag is set, then building will only be activated on click and will not give its effect on hero recrutiment, hero visit, or new day. This allows implementing changes to Mana Vortex from HotA
954 lines
30 KiB
C++
954 lines
30 KiB
C++
/*
|
|
* CTownHandler.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 "CTownHandler.h"
|
|
|
|
#include "CTown.h"
|
|
#include "CFaction.h"
|
|
#include "../building/CBuilding.h"
|
|
|
|
#include "../../CCreatureHandler.h"
|
|
#include "../../CHeroHandler.h"
|
|
#include "../../GameSettings.h"
|
|
#include "../../TerrainHandler.h"
|
|
#include "../../VCMI_Lib.h"
|
|
|
|
#include "../../bonuses/Propagators.h"
|
|
#include "../../constants/StringConstants.h"
|
|
#include "../../mapObjectConstructors/AObjectTypeHandler.h"
|
|
#include "../../mapObjectConstructors/CObjectClassesHandler.h"
|
|
#include "../../modding/IdentifierStorage.h"
|
|
#include "../../modding/ModScope.h"
|
|
#include "../../spells/CSpellHandler.h"
|
|
#include "../../texts/CGeneralTextHandler.h"
|
|
#include "../../texts/CLegacyConfigParser.h"
|
|
#include "../../json/JsonBonus.h"
|
|
#include "../../json/JsonUtils.h"
|
|
|
|
VCMI_LIB_NAMESPACE_BEGIN
|
|
|
|
const int NAMES_PER_TOWN=16; // number of town names per faction in H3 files. Json can define any number
|
|
|
|
CTownHandler::CTownHandler()
|
|
: buildingsLibrary(JsonPath::builtin("config/buildingsLibrary"))
|
|
, randomTown(new CTown())
|
|
, randomFaction(new CFaction())
|
|
{
|
|
randomFaction->town = randomTown;
|
|
randomTown->faction = randomFaction;
|
|
randomFaction->identifier = "random";
|
|
randomFaction->modScope = "core";
|
|
}
|
|
|
|
CTownHandler::~CTownHandler()
|
|
{
|
|
delete randomFaction; // will also delete randomTown
|
|
}
|
|
|
|
JsonNode readBuilding(CLegacyConfigParser & parser)
|
|
{
|
|
JsonNode ret;
|
|
JsonNode & cost = ret["cost"];
|
|
|
|
//note: this code will try to parse mithril as well but wil always return 0 for it
|
|
for(const std::string & resID : GameConstants::RESOURCE_NAMES)
|
|
cost[resID].Float() = parser.readNumber();
|
|
|
|
cost.Struct().erase("mithril"); // erase mithril to avoid confusing validator
|
|
|
|
parser.endLine();
|
|
|
|
return ret;
|
|
}
|
|
|
|
const TPropagatorPtr & CTownHandler::emptyPropagator()
|
|
{
|
|
static const TPropagatorPtr emptyProp(nullptr);
|
|
return emptyProp;
|
|
}
|
|
|
|
std::vector<JsonNode> CTownHandler::loadLegacyData()
|
|
{
|
|
size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_FACTION);
|
|
|
|
std::vector<JsonNode> dest(dataSize);
|
|
objects.resize(dataSize);
|
|
|
|
auto getBuild = [&](size_t town, size_t building) -> JsonNode &
|
|
{
|
|
return dest[town]["town"]["buildings"][EBuildingType::names[building]];
|
|
};
|
|
|
|
CLegacyConfigParser parser(TextPath::builtin("DATA/BUILDING.TXT"));
|
|
|
|
parser.endLine(); // header
|
|
parser.endLine();
|
|
|
|
//Unique buildings
|
|
for (size_t town=0; town<dataSize; town++)
|
|
{
|
|
parser.endLine(); //header
|
|
parser.endLine();
|
|
|
|
int buildID = 17;
|
|
do
|
|
{
|
|
getBuild(town, buildID) = readBuilding(parser);
|
|
buildID++;
|
|
}
|
|
while (!parser.isNextEntryEmpty());
|
|
}
|
|
|
|
// Common buildings
|
|
parser.endLine(); // header
|
|
parser.endLine();
|
|
parser.endLine();
|
|
|
|
int buildID = 0;
|
|
do
|
|
{
|
|
JsonNode building = readBuilding(parser);
|
|
|
|
for (size_t town=0; town<dataSize; town++)
|
|
getBuild(town, buildID) = building;
|
|
|
|
buildID++;
|
|
}
|
|
while (!parser.isNextEntryEmpty());
|
|
|
|
parser.endLine(); //header
|
|
parser.endLine();
|
|
|
|
//Dwellings
|
|
for (size_t town=0; town<dataSize; town++)
|
|
{
|
|
parser.endLine(); //header
|
|
parser.endLine();
|
|
|
|
for (size_t i=0; i<14; i++)
|
|
{
|
|
getBuild(town, 30+i) = readBuilding(parser);
|
|
}
|
|
}
|
|
{
|
|
CLegacyConfigParser parser(TextPath::builtin("DATA/BLDGNEUT.TXT"));
|
|
|
|
for(int building=0; building<15; building++)
|
|
{
|
|
std::string name = parser.readString();
|
|
std::string descr = parser.readString();
|
|
parser.endLine();
|
|
|
|
for(int j=0; j<dataSize; j++)
|
|
{
|
|
getBuild(j, building)["name"].String() = name;
|
|
getBuild(j, building)["description"].String() = descr;
|
|
}
|
|
}
|
|
parser.endLine(); // silo
|
|
parser.endLine(); // blacksmith //unused entries
|
|
parser.endLine(); // moat
|
|
|
|
//shipyard with the ship
|
|
std::string name = parser.readString();
|
|
std::string descr = parser.readString();
|
|
parser.endLine();
|
|
|
|
for(int town=0; town<dataSize; town++)
|
|
{
|
|
getBuild(town, 20)["name"].String() = name;
|
|
getBuild(town, 20)["description"].String() = descr;
|
|
}
|
|
|
|
//blacksmith
|
|
for(int town=0; town<dataSize; town++)
|
|
{
|
|
getBuild(town, 16)["name"].String() = parser.readString();
|
|
getBuild(town, 16)["description"].String() = parser.readString();
|
|
parser.endLine();
|
|
}
|
|
}
|
|
{
|
|
CLegacyConfigParser parser(TextPath::builtin("DATA/BLDGSPEC.TXT"));
|
|
|
|
for(int town=0; town<dataSize; town++)
|
|
{
|
|
for(int build=0; build<9; build++)
|
|
{
|
|
getBuild(town, 17 + build)["name"].String() = parser.readString();
|
|
getBuild(town, 17 + build)["description"].String() = parser.readString();
|
|
parser.endLine();
|
|
}
|
|
getBuild(town, 26)["name"].String() = parser.readString(); // Grail
|
|
getBuild(town, 26)["description"].String() = parser.readString();
|
|
parser.endLine();
|
|
|
|
getBuild(town, 15)["name"].String() = parser.readString(); // Resource silo
|
|
getBuild(town, 15)["description"].String() = parser.readString();
|
|
parser.endLine();
|
|
}
|
|
}
|
|
{
|
|
CLegacyConfigParser parser(TextPath::builtin("DATA/DWELLING.TXT"));
|
|
|
|
for(int town=0; town<dataSize; town++)
|
|
{
|
|
for(int build=0; build<14; build++)
|
|
{
|
|
getBuild(town, 30 + build)["name"].String() = parser.readString();
|
|
getBuild(town, 30 + build)["description"].String() = parser.readString();
|
|
parser.endLine();
|
|
}
|
|
}
|
|
}
|
|
{
|
|
CLegacyConfigParser typeParser(TextPath::builtin("DATA/TOWNTYPE.TXT"));
|
|
CLegacyConfigParser nameParser(TextPath::builtin("DATA/TOWNNAME.TXT"));
|
|
size_t townID=0;
|
|
do
|
|
{
|
|
dest[townID]["name"].String() = typeParser.readString();
|
|
|
|
for (int i=0; i<NAMES_PER_TOWN; i++)
|
|
{
|
|
JsonNode name;
|
|
name.String() = nameParser.readString();
|
|
dest[townID]["town"]["names"].Vector().push_back(name);
|
|
nameParser.endLine();
|
|
}
|
|
townID++;
|
|
}
|
|
while (typeParser.endLine());
|
|
}
|
|
return dest;
|
|
}
|
|
|
|
void CTownHandler::loadBuildingRequirements(CBuilding * building, const JsonNode & source, std::vector<BuildingRequirementsHelper> & bidsToLoad) const
|
|
{
|
|
if (source.isNull())
|
|
return;
|
|
|
|
BuildingRequirementsHelper hlp;
|
|
hlp.building = building;
|
|
hlp.town = building->town;
|
|
hlp.json = source;
|
|
bidsToLoad.push_back(hlp);
|
|
}
|
|
|
|
void CTownHandler::loadBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building) const
|
|
{
|
|
for(const auto & b : source.Vector())
|
|
{
|
|
auto bonus = std::make_shared<Bonus>(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, BonusSourceID(building->getUniqueTypeID()));
|
|
|
|
if(!JsonUtils::parseBonus(b, bonus.get()))
|
|
continue;
|
|
|
|
bonus->description.appendTextID(building->getNameTextID());
|
|
|
|
//JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty.
|
|
assert(bonus->propagator == nullptr || bonus->propagator->getPropagatorType() != CBonusSystemNode::ENodeTypes::UNKNOWN);
|
|
|
|
if(bonus->propagator != nullptr
|
|
&& bonus->propagator->getPropagatorType() == CBonusSystemNode::ENodeTypes::UNKNOWN)
|
|
bonus->addPropagator(emptyPropagator());
|
|
building->addNewBonus(bonus, bonusList);
|
|
}
|
|
}
|
|
|
|
void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source)
|
|
{
|
|
assert(stringID.find(':') == std::string::npos);
|
|
assert(!source.getModScope().empty());
|
|
|
|
auto * ret = new CBuilding();
|
|
ret->bid = vstd::find_or(MappedKeys::BUILDING_NAMES_TO_TYPES, stringID, BuildingID::NONE);
|
|
ret->subId = BuildingSubID::NONE;
|
|
|
|
if(ret->bid == BuildingID::NONE && !source["id"].isNull())
|
|
{
|
|
// FIXME: A lot of false-positives with no clear way to handle them in mods
|
|
//logMod->warn("Building %s: id field is deprecated", stringID);
|
|
ret->bid = source["id"].isNull() ? BuildingID(BuildingID::NONE) : BuildingID(source["id"].Float());
|
|
}
|
|
|
|
if (ret->bid == BuildingID::NONE)
|
|
logMod->error("Building '%s' isn't recognized and won't work properly. Correct the typo or update VCMI.", stringID);
|
|
|
|
ret->mode = ret->bid == BuildingID::GRAIL
|
|
? CBuilding::BUILD_GRAIL
|
|
: vstd::find_or(CBuilding::MODES, source["mode"].String(), CBuilding::BUILD_NORMAL);
|
|
|
|
ret->height = vstd::find_or(CBuilding::TOWER_TYPES, source["height"].String(), CBuilding::HEIGHT_NO_TOWER);
|
|
|
|
ret->identifier = stringID;
|
|
ret->modScope = source.getModScope();
|
|
ret->town = town;
|
|
|
|
VLC->generaltexth->registerString(source.getModScope(), ret->getNameTextID(), source["name"].String());
|
|
VLC->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"].String());
|
|
|
|
ret->subId = vstd::find_or(MappedKeys::SPECIAL_BUILDINGS, source["type"].String(), BuildingSubID::NONE);
|
|
ret->resources = TResources(source["cost"]);
|
|
ret->produce = TResources(source["produce"]);
|
|
|
|
ret->manualHeroVisit = source["manualHeroVisit"].Bool();
|
|
ret->upgradeReplacesBonuses = source["upgradeReplacesBonuses"].Bool();
|
|
|
|
const JsonNode & fortifications = source["fortifications"];
|
|
if (!fortifications.isNull())
|
|
{
|
|
VLC->identifiers()->requestIdentifierOptional("creature", fortifications["citadelShooter"], [=](si32 identifier)
|
|
{
|
|
ret->fortifications.citadelShooter = CreatureID(identifier);
|
|
});
|
|
|
|
VLC->identifiers()->requestIdentifierOptional("creature", fortifications["upperTowerShooter"], [=](si32 identifier)
|
|
{
|
|
ret->fortifications.upperTowerShooter = CreatureID(identifier);
|
|
});
|
|
|
|
VLC->identifiers()->requestIdentifierOptional("creature", fortifications["lowerTowerShooter"], [=](si32 identifier)
|
|
{
|
|
ret->fortifications.lowerTowerShooter = CreatureID(identifier);
|
|
});
|
|
|
|
ret->fortifications.wallsHealth = fortifications["wallsHealth"].Integer();
|
|
ret->fortifications.citadelHealth = fortifications["citadelHealth"].Integer();
|
|
ret->fortifications.upperTowerHealth = fortifications["upperTowerHealth"].Integer();
|
|
ret->fortifications.lowerTowerHealth = fortifications["lowerTowerHealth"].Integer();
|
|
ret->fortifications.hasMoat = fortifications["hasMoat"].Bool();
|
|
}
|
|
|
|
loadBuildingBonuses(source["bonuses"], ret->buildingBonuses, ret);
|
|
|
|
if(!source["configuration"].isNull())
|
|
ret->rewardableObjectInfo.init(source["configuration"], ret->getBaseTextID());
|
|
|
|
//MODS COMPATIBILITY FOR pre-1.6
|
|
if(ret->produce.empty() && ret->bid == BuildingID::RESOURCE_SILO)
|
|
{
|
|
logGlobal->warn("Resource silo in town '%s' does not produces any resources!", ret->town->faction->getJsonKey());
|
|
switch (ret->town->primaryRes.toEnum())
|
|
{
|
|
case EGameResID::GOLD:
|
|
ret->produce[ret->town->primaryRes] = 500;
|
|
break;
|
|
case EGameResID::WOOD_AND_ORE:
|
|
ret->produce[EGameResID::WOOD] = 1;
|
|
ret->produce[EGameResID::ORE] = 1;
|
|
break;
|
|
default:
|
|
ret->produce[ret->town->primaryRes] = 1;
|
|
break;
|
|
}
|
|
}
|
|
loadBuildingRequirements(ret, source["requires"], requirementsToLoad);
|
|
|
|
if (!source["warMachine"].isNull())
|
|
{
|
|
VLC->identifiers()->requestIdentifier("artifact", source["warMachine"], [=](si32 identifier)
|
|
{
|
|
ret->warMachine = ArtifactID(identifier);
|
|
});
|
|
}
|
|
|
|
if (!source["upgrades"].isNull())
|
|
{
|
|
// building id and upgrades can't be the same
|
|
if(stringID == source["upgrades"].String())
|
|
{
|
|
throw std::runtime_error(boost::str(boost::format("Building with ID '%s' of town '%s' can't be an upgrade of the same building.") %
|
|
stringID % ret->town->faction->getNameTranslated()));
|
|
}
|
|
|
|
VLC->identifiers()->requestIdentifier(ret->town->getBuildingScope(), source["upgrades"], [=](si32 identifier)
|
|
{
|
|
ret->upgrade = BuildingID(identifier);
|
|
});
|
|
}
|
|
else
|
|
ret->upgrade = BuildingID::NONE;
|
|
|
|
ret->town->buildings[ret->bid] = ret;
|
|
for(const auto & element : source["marketModes"].Vector())
|
|
{
|
|
if(MappedKeys::MARKET_NAMES_TO_TYPES.count(element.String()))
|
|
ret->marketModes.insert(MappedKeys::MARKET_NAMES_TO_TYPES.at(element.String()));
|
|
}
|
|
|
|
registerObject(source.getModScope(), ret->town->getBuildingScope(), ret->identifier, ret->bid.getNum());
|
|
}
|
|
|
|
void CTownHandler::loadBuildings(CTown * town, const JsonNode & source)
|
|
{
|
|
if(source.isStruct())
|
|
{
|
|
for(const auto & node : source.Struct())
|
|
{
|
|
if (!node.second.isNull())
|
|
loadBuilding(town, node.first, node.second);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CTownHandler::loadStructure(CTown &town, const std::string & stringID, const JsonNode & source) const
|
|
{
|
|
auto * ret = new CStructure();
|
|
|
|
ret->building = nullptr;
|
|
ret->buildable = nullptr;
|
|
|
|
VLC->identifiers()->tryRequestIdentifier( source.getModScope(), "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable
|
|
{
|
|
ret->building = town.buildings[BuildingID(identifier)];
|
|
});
|
|
|
|
if (source["builds"].isNull())
|
|
{
|
|
VLC->identifiers()->tryRequestIdentifier( source.getModScope(), "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable
|
|
{
|
|
ret->building = town.buildings[BuildingID(identifier)];
|
|
});
|
|
}
|
|
else
|
|
{
|
|
VLC->identifiers()->requestIdentifier("building." + town.faction->getJsonKey(), source["builds"], [=, &town](si32 identifier) mutable
|
|
{
|
|
ret->buildable = town.buildings[BuildingID(identifier)];
|
|
});
|
|
}
|
|
|
|
ret->identifier = stringID;
|
|
ret->pos.x = static_cast<si32>(source["x"].Float());
|
|
ret->pos.y = static_cast<si32>(source["y"].Float());
|
|
ret->pos.z = static_cast<si32>(source["z"].Float());
|
|
|
|
ret->hiddenUpgrade = source["hidden"].Bool();
|
|
ret->defName = AnimationPath::fromJson(source["animation"]);
|
|
ret->borderName = ImagePath::fromJson(source["border"]);
|
|
ret->areaName = ImagePath::fromJson(source["area"]);
|
|
|
|
town.clientInfo.structures.emplace_back(ret);
|
|
}
|
|
|
|
void CTownHandler::loadStructures(CTown &town, const JsonNode & source) const
|
|
{
|
|
for(const auto & node : source.Struct())
|
|
{
|
|
if (!node.second.isNull())
|
|
loadStructure(town, node.first, node.second);
|
|
}
|
|
}
|
|
|
|
void CTownHandler::loadTownHall(CTown &town, const JsonNode & source) const
|
|
{
|
|
auto & dstSlots = town.clientInfo.hallSlots;
|
|
const auto & srcSlots = source.Vector();
|
|
dstSlots.resize(srcSlots.size());
|
|
|
|
for(size_t i=0; i<dstSlots.size(); i++)
|
|
{
|
|
auto & dstRow = dstSlots[i];
|
|
const auto & srcRow = srcSlots[i].Vector();
|
|
dstRow.resize(srcRow.size());
|
|
|
|
for(size_t j=0; j < dstRow.size(); j++)
|
|
{
|
|
auto & dstBox = dstRow[j];
|
|
const auto & srcBox = srcRow[j].Vector();
|
|
dstBox.resize(srcBox.size());
|
|
|
|
for(size_t k=0; k<dstBox.size(); k++)
|
|
{
|
|
auto & dst = dstBox[k];
|
|
const auto & src = srcBox[k];
|
|
|
|
VLC->identifiers()->requestIdentifier("building." + town.faction->getJsonKey(), src, [&](si32 identifier)
|
|
{
|
|
dst = BuildingID(identifier);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Point JsonToPoint(const JsonNode & node)
|
|
{
|
|
if(!node.isStruct())
|
|
return Point::makeInvalid();
|
|
|
|
Point ret;
|
|
ret.x = static_cast<si32>(node["x"].Float());
|
|
ret.y = static_cast<si32>(node["y"].Float());
|
|
return ret;
|
|
}
|
|
|
|
void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source) const
|
|
{
|
|
town.clientInfo.siegePrefix = source["imagePrefix"].String();
|
|
town.clientInfo.towerIconSmall = source["towerIconSmall"].String();
|
|
town.clientInfo.towerIconLarge = source["towerIconLarge"].String();
|
|
|
|
VLC->identifiers()->requestIdentifier("creature", source["shooter"], [&town](si32 creature)
|
|
{
|
|
auto crId = CreatureID(creature);
|
|
if((*VLC->creh)[crId]->animation.missileFrameAngles.empty())
|
|
logMod->error("Mod '%s' error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Siege will not work properly!"
|
|
, town.faction->getNameTranslated()
|
|
, (*VLC->creh)[crId]->getNameSingularTranslated());
|
|
|
|
town.fortifications.citadelShooter = crId;
|
|
town.fortifications.upperTowerShooter = crId;
|
|
town.fortifications.lowerTowerShooter = crId;
|
|
});
|
|
|
|
auto & pos = town.clientInfo.siegePositions;
|
|
pos.resize(21);
|
|
|
|
pos[8] = JsonToPoint(source["towers"]["top"]["tower"]);
|
|
pos[17] = JsonToPoint(source["towers"]["top"]["battlement"]);
|
|
pos[20] = JsonToPoint(source["towers"]["top"]["creature"]);
|
|
|
|
pos[2] = JsonToPoint(source["towers"]["keep"]["tower"]);
|
|
pos[15] = JsonToPoint(source["towers"]["keep"]["battlement"]);
|
|
pos[18] = JsonToPoint(source["towers"]["keep"]["creature"]);
|
|
|
|
pos[3] = JsonToPoint(source["towers"]["bottom"]["tower"]);
|
|
pos[16] = JsonToPoint(source["towers"]["bottom"]["battlement"]);
|
|
pos[19] = JsonToPoint(source["towers"]["bottom"]["creature"]);
|
|
|
|
pos[9] = JsonToPoint(source["gate"]["gate"]);
|
|
pos[10] = JsonToPoint(source["gate"]["arch"]);
|
|
|
|
pos[7] = JsonToPoint(source["walls"]["upper"]);
|
|
pos[6] = JsonToPoint(source["walls"]["upperMid"]);
|
|
pos[5] = JsonToPoint(source["walls"]["bottomMid"]);
|
|
pos[4] = JsonToPoint(source["walls"]["bottom"]);
|
|
|
|
pos[13] = JsonToPoint(source["moat"]["moat"]);
|
|
pos[14] = JsonToPoint(source["moat"]["bank"]);
|
|
|
|
pos[11] = JsonToPoint(source["static"]["bottom"]);
|
|
pos[12] = JsonToPoint(source["static"]["top"]);
|
|
pos[1] = JsonToPoint(source["static"]["background"]);
|
|
}
|
|
|
|
static void readIcon(JsonNode source, std::string & small, std::string & large)
|
|
{
|
|
if (source.getType() == JsonNode::JsonType::DATA_STRUCT) // don't crash on old format
|
|
{
|
|
small = source["small"].String();
|
|
large = source["large"].String();
|
|
}
|
|
}
|
|
|
|
void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const
|
|
{
|
|
CTown::ClientInfo & info = town.clientInfo;
|
|
|
|
readIcon(source["icons"]["village"]["normal"], info.iconSmall[0][0], info.iconLarge[0][0]);
|
|
readIcon(source["icons"]["village"]["built"], info.iconSmall[0][1], info.iconLarge[0][1]);
|
|
readIcon(source["icons"]["fort"]["normal"], info.iconSmall[1][0], info.iconLarge[1][0]);
|
|
readIcon(source["icons"]["fort"]["built"], info.iconSmall[1][1], info.iconLarge[1][1]);
|
|
|
|
if (source["musicTheme"].isVector())
|
|
{
|
|
for (auto const & entry : source["musicTheme"].Vector())
|
|
info.musicTheme.push_back(AudioPath::fromJson(entry));
|
|
}
|
|
else
|
|
{
|
|
info.musicTheme.push_back(AudioPath::fromJson(source["musicTheme"]));
|
|
}
|
|
|
|
info.hallBackground = ImagePath::fromJson(source["hallBackground"]);
|
|
info.townBackground = ImagePath::fromJson(source["townBackground"]);
|
|
info.guildWindow = ImagePath::fromJson(source["guildWindow"]);
|
|
info.buildingsIcons = AnimationPath::fromJson(source["buildingsIcons"]);
|
|
|
|
info.guildBackground = ImagePath::fromJson(source["guildBackground"]);
|
|
info.tavernVideo = VideoPath::fromJson(source["tavernVideo"]);
|
|
|
|
loadTownHall(town, source["hallSlots"]);
|
|
loadStructures(town, source["structures"]);
|
|
loadSiegeScreen(town, source["siege"]);
|
|
}
|
|
|
|
void CTownHandler::loadTown(CTown * town, const JsonNode & source)
|
|
{
|
|
const auto * resIter = boost::find(GameConstants::RESOURCE_NAMES, source["primaryResource"].String());
|
|
if(resIter == std::end(GameConstants::RESOURCE_NAMES))
|
|
town->primaryRes = GameResID(EGameResID::WOOD_AND_ORE); //Wood + Ore
|
|
else
|
|
town->primaryRes = GameResID(resIter - std::begin(GameConstants::RESOURCE_NAMES));
|
|
|
|
if (!source["warMachine"].isNull())
|
|
{
|
|
VLC->identifiers()->requestIdentifier( "creature", source["warMachine"], [=](si32 creatureID)
|
|
{
|
|
town->warMachineDeprecated = creatureID;
|
|
});
|
|
}
|
|
|
|
town->mageLevel = static_cast<ui32>(source["mageGuild"].Float());
|
|
|
|
town->namesCount = 0;
|
|
for(const auto & name : source["names"].Vector())
|
|
{
|
|
VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name.String());
|
|
town->namesCount += 1;
|
|
}
|
|
|
|
if (!source["moatAbility"].isNull()) // VCMI 1.2 compatibility code
|
|
{
|
|
VLC->identifiers()->requestIdentifier( "spell", source["moatAbility"], [=](si32 ability)
|
|
{
|
|
town->fortifications.moatSpell = SpellID(ability);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
VLC->identifiers()->requestIdentifier( source.getModScope(), "spell", "castleMoat", [=](si32 ability)
|
|
{
|
|
town->fortifications.moatSpell = SpellID(ability);
|
|
});
|
|
}
|
|
|
|
// Horde building creature level
|
|
for(const JsonNode &node : source["horde"].Vector())
|
|
town->hordeLvl[static_cast<int>(town->hordeLvl.size())] = static_cast<int>(node.Float());
|
|
|
|
// town needs to have exactly 2 horde entries. Validation will take care of 2+ entries
|
|
// but anything below 2 must be handled here
|
|
for (size_t i=source["horde"].Vector().size(); i<2; i++)
|
|
town->hordeLvl[static_cast<int>(i)] = -1;
|
|
|
|
const JsonVector & creatures = source["creatures"].Vector();
|
|
|
|
town->creatures.resize(creatures.size());
|
|
|
|
for (size_t i=0; i< creatures.size(); i++)
|
|
{
|
|
const JsonVector & level = creatures[i].Vector();
|
|
|
|
town->creatures[i].resize(level.size());
|
|
|
|
for (size_t j=0; j<level.size(); j++)
|
|
{
|
|
VLC->identifiers()->requestIdentifier("creature", level[j], [=](si32 creature)
|
|
{
|
|
town->creatures[i][j] = CreatureID(creature);
|
|
});
|
|
}
|
|
}
|
|
|
|
town->defaultTavernChance = static_cast<ui32>(source["defaultTavern"].Float());
|
|
/// set chance of specific hero class to appear in this town
|
|
for(const auto & node : source["tavern"].Struct())
|
|
{
|
|
int chance = static_cast<int>(node.second.Float());
|
|
|
|
VLC->identifiers()->requestIdentifier(node.second.getModScope(), "heroClass",node.first, [=](si32 classID)
|
|
{
|
|
VLC->heroclassesh->objects[classID]->selectionProbability[town->faction->getId()] = chance;
|
|
});
|
|
}
|
|
|
|
for(const auto & node : source["guildSpells"].Struct())
|
|
{
|
|
int chance = static_cast<int>(node.second.Float());
|
|
|
|
VLC->identifiers()->requestIdentifier(node.second.getModScope(), "spell", node.first, [=](si32 spellID)
|
|
{
|
|
VLC->spellh->objects.at(spellID)->probabilities[town->faction->getId()] = chance;
|
|
});
|
|
}
|
|
|
|
for(const JsonNode & d : source["adventureMap"]["dwellings"].Vector())
|
|
{
|
|
town->dwellings.push_back(d["graphics"].String());
|
|
town->dwellingNames.push_back(d["name"].String());
|
|
}
|
|
|
|
loadBuildings(town, source["buildings"]);
|
|
loadClientData(*town, source);
|
|
}
|
|
|
|
void CTownHandler::loadPuzzle(CFaction &faction, const JsonNode &source) const
|
|
{
|
|
faction.puzzleMap.reserve(GameConstants::PUZZLE_MAP_PIECES);
|
|
|
|
std::string prefix = source["prefix"].String();
|
|
for(const JsonNode &piece : source["pieces"].Vector())
|
|
{
|
|
size_t index = faction.puzzleMap.size();
|
|
SPuzzleInfo spi;
|
|
|
|
spi.position.x = static_cast<si16>(piece["x"].Float());
|
|
spi.position.y = static_cast<si16>(piece["y"].Float());
|
|
spi.whenUncovered = static_cast<ui16>(piece["index"].Float());
|
|
spi.number = static_cast<ui16>(index);
|
|
|
|
// filename calculation
|
|
std::ostringstream suffix;
|
|
suffix << std::setfill('0') << std::setw(2) << index;
|
|
|
|
spi.filename = ImagePath::builtinTODO(prefix + suffix.str());
|
|
|
|
faction.puzzleMap.push_back(spi);
|
|
}
|
|
assert(faction.puzzleMap.size() == GameConstants::PUZZLE_MAP_PIECES);
|
|
}
|
|
|
|
std::shared_ptr<CFaction> CTownHandler::loadFromJson(const std::string & scope, const JsonNode & source, const std::string & identifier, size_t index)
|
|
{
|
|
assert(identifier.find(':') == std::string::npos);
|
|
|
|
auto faction = std::make_shared<CFaction>();
|
|
|
|
faction->index = static_cast<FactionID>(index);
|
|
faction->modScope = scope;
|
|
faction->identifier = identifier;
|
|
|
|
VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String());
|
|
VLC->generaltexth->registerString(scope, faction->getDescriptionTranslated(), source["description"].String());
|
|
|
|
faction->creatureBg120 = ImagePath::fromJson(source["creatureBackground"]["120px"]);
|
|
faction->creatureBg130 = ImagePath::fromJson(source["creatureBackground"]["130px"]);
|
|
|
|
faction->boatType = BoatId::CASTLE; //Do not crash
|
|
if (!source["boat"].isNull())
|
|
{
|
|
VLC->identifiers()->requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID)
|
|
{
|
|
faction->boatType = BoatId(boatTypeID);
|
|
});
|
|
}
|
|
|
|
int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, source["alignment"].String());
|
|
if (alignment == -1)
|
|
faction->alignment = EAlignment::NEUTRAL;
|
|
else
|
|
faction->alignment = static_cast<EAlignment>(alignment);
|
|
|
|
auto preferUndergound = source["preferUndergroundPlacement"];
|
|
faction->preferUndergroundPlacement = preferUndergound.isNull() ? false : preferUndergound.Bool();
|
|
faction->special = source["special"].Bool();
|
|
|
|
// NOTE: semi-workaround - normally, towns are supposed to have native terrains.
|
|
// Towns without one are exceptions. So, vcmi requires nativeTerrain to be defined
|
|
// But allows it to be defined with explicit value of "none" if town should not have native terrain
|
|
// This is better than allowing such terrain-less towns silently, leading to issues with RMG
|
|
faction->nativeTerrain = ETerrainId::NONE;
|
|
if ( !source["nativeTerrain"].isNull() && source["nativeTerrain"].String() != "none")
|
|
{
|
|
VLC->identifiers()->requestIdentifier("terrain", source["nativeTerrain"], [=](int32_t index){
|
|
faction->nativeTerrain = TerrainId(index);
|
|
|
|
auto const & terrain = VLC->terrainTypeHandler->getById(faction->nativeTerrain);
|
|
|
|
if (!terrain->isSurface() && !terrain->isUnderground())
|
|
logMod->warn("Faction %s has terrain %s as native, but terrain is not suitable for either surface or subterranean layers!", faction->getJsonKey(), terrain->getJsonKey());
|
|
});
|
|
}
|
|
|
|
if (!source["town"].isNull())
|
|
{
|
|
faction->town = new CTown();
|
|
faction->town->faction = faction.get();
|
|
loadTown(faction->town, source["town"]);
|
|
}
|
|
else
|
|
faction->town = nullptr;
|
|
|
|
if (!source["puzzleMap"].isNull())
|
|
loadPuzzle(*faction, source["puzzleMap"]);
|
|
|
|
return faction;
|
|
}
|
|
|
|
void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data)
|
|
{
|
|
auto object = loadFromJson(scope, data, name, objects.size());
|
|
|
|
objects.emplace_back(object);
|
|
|
|
if (object->town)
|
|
{
|
|
auto & info = object->town->clientInfo;
|
|
info.icons[0][0] = 8 + object->index.getNum() * 4 + 0;
|
|
info.icons[0][1] = 8 + object->index.getNum() * 4 + 1;
|
|
info.icons[1][0] = 8 + object->index.getNum() * 4 + 2;
|
|
info.icons[1][1] = 8 + object->index.getNum() * 4 + 3;
|
|
|
|
VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index)
|
|
{
|
|
// register town once objects are loaded
|
|
JsonNode config = data["town"]["mapObject"];
|
|
config["faction"].String() = name;
|
|
config["faction"].setModScope(scope, false);
|
|
if (config.getModScope().empty())// MODS COMPATIBILITY FOR 0.96
|
|
config.setModScope(scope, false);
|
|
VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index);
|
|
|
|
// MODS COMPATIBILITY FOR 0.96
|
|
const auto & advMap = data["town"]["adventureMap"];
|
|
if (!advMap.isNull())
|
|
{
|
|
logMod->warn("Outdated town mod. Will try to generate valid templates out of fort");
|
|
JsonNode config;
|
|
config["animation"] = advMap["castle"];
|
|
VLC->objtypeh->getHandlerFor(index, object->index)->addTemplate(config);
|
|
}
|
|
});
|
|
}
|
|
|
|
registerObject(scope, "faction", name, object->index.getNum());
|
|
}
|
|
|
|
void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index)
|
|
{
|
|
auto object = loadFromJson(scope, data, name, index);
|
|
|
|
if (objects.size() > index)
|
|
assert(objects[index] == nullptr); // ensure that this id was not loaded before
|
|
else
|
|
objects.resize(index + 1);
|
|
objects[index] = object;
|
|
|
|
if (object->town)
|
|
{
|
|
auto & info = object->town->clientInfo;
|
|
info.icons[0][0] = (GameConstants::F_NUMBER + object->index.getNum()) * 2 + 0;
|
|
info.icons[0][1] = (GameConstants::F_NUMBER + object->index.getNum()) * 2 + 1;
|
|
info.icons[1][0] = object->index.getNum() * 2 + 0;
|
|
info.icons[1][1] = object->index.getNum() * 2 + 1;
|
|
|
|
VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index)
|
|
{
|
|
// register town once objects are loaded
|
|
JsonNode config = data["town"]["mapObject"];
|
|
config["faction"].String() = name;
|
|
config["faction"].setModScope(scope, false);
|
|
VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index);
|
|
});
|
|
}
|
|
|
|
registerObject(scope, "faction", name, object->index.getNum());
|
|
}
|
|
|
|
void CTownHandler::loadRandomFaction()
|
|
{
|
|
JsonNode randomFactionJson(JsonPath::builtin("config/factions/random.json"));
|
|
randomFactionJson.setModScope(ModScope::scopeBuiltin(), true);
|
|
loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]);
|
|
}
|
|
|
|
void CTownHandler::loadCustom()
|
|
{
|
|
loadRandomFaction();
|
|
}
|
|
|
|
void CTownHandler::beforeValidate(JsonNode & object)
|
|
{
|
|
if (object.Struct().count("town") == 0)
|
|
return;
|
|
|
|
const auto & inheritBuilding = [this](const std::string & name, JsonNode & target)
|
|
{
|
|
if (buildingsLibrary.Struct().count(name) == 0)
|
|
return;
|
|
|
|
JsonNode baseCopy(buildingsLibrary[name]);
|
|
baseCopy.setModScope(target.getModScope());
|
|
JsonUtils::inherit(target, baseCopy);
|
|
};
|
|
|
|
for (auto & building : object["town"]["buildings"].Struct())
|
|
{
|
|
inheritBuilding(building.first, building.second);
|
|
if (building.second.Struct().count("type"))
|
|
inheritBuilding(building.second["type"].String(), building.second);
|
|
|
|
// MODS COMPATIBILITY FOR pre-1.6
|
|
// convert old buildigns with onVisitBonuses into configurable building
|
|
if (building.second.Struct().count("onVisitBonuses"))
|
|
{
|
|
building.second["configuration"]["visitMode"] = JsonNode("bonus");
|
|
building.second["configuration"]["visitMode"]["rewards"][0]["message"] = building.second["description"];
|
|
building.second["configuration"]["visitMode"]["rewards"][0]["bonuses"] = building.second["onVisitBonuses"];
|
|
}
|
|
}
|
|
}
|
|
|
|
void CTownHandler::afterLoadFinalization()
|
|
{
|
|
initializeRequirements();
|
|
}
|
|
|
|
void CTownHandler::initializeRequirements()
|
|
{
|
|
// must be done separately after all ID's are known
|
|
for (auto & requirement : requirementsToLoad)
|
|
{
|
|
requirement.building->requirements = CBuilding::TRequired(requirement.json, [&](const JsonNode & node) -> BuildingID
|
|
{
|
|
if (node.Vector().size() > 1)
|
|
{
|
|
logMod->error("Unexpected length of town buildings requirements: %d", node.Vector().size());
|
|
logMod->error("Entry contains: ");
|
|
logMod->error(node.toString());
|
|
}
|
|
|
|
auto index = VLC->identifiers()->getIdentifier(requirement.town->getBuildingScope(), node[0]);
|
|
|
|
if (!index.has_value())
|
|
{
|
|
logMod->error("Unknown building in town buildings: %s", node[0].String());
|
|
return BuildingID::NONE;
|
|
}
|
|
return BuildingID(index.value());
|
|
});
|
|
}
|
|
requirementsToLoad.clear();
|
|
}
|
|
|
|
std::set<FactionID> CTownHandler::getDefaultAllowed() const
|
|
{
|
|
std::set<FactionID> allowedFactions;
|
|
|
|
for(const auto & town : objects)
|
|
if (town->town != nullptr && !town->special)
|
|
allowedFactions.insert(town->getId());
|
|
|
|
return allowedFactions;
|
|
}
|
|
|
|
std::set<FactionID> CTownHandler::getAllowedFactions(bool withTown) const
|
|
{
|
|
if (withTown)
|
|
return getDefaultAllowed();
|
|
|
|
std::set<FactionID> result;
|
|
for(const auto & town : objects)
|
|
result.insert(town->getId());
|
|
|
|
return result;
|
|
}
|
|
|
|
const std::vector<std::string> & CTownHandler::getTypeNames() const
|
|
{
|
|
static const std::vector<std::string> typeNames = { "faction", "town" };
|
|
return typeNames;
|
|
}
|
|
|
|
VCMI_LIB_NAMESPACE_END
|