Browse Source

Merge remote-tracking branch 'upstream/develop' into develop

Xilmi 1 year ago
parent
commit
27aed74397

+ 10 - 8
Mods/vcmi/config/vcmi/portuguese.json

@@ -3,9 +3,9 @@
 	"vcmi.adventureMap.monsterThreat.levels.0"  : "Sem Esforço",
 	"vcmi.adventureMap.monsterThreat.levels.1"  : "Muito Fraca",
 	"vcmi.adventureMap.monsterThreat.levels.2"  : "Fraca",
-	"vcmi.adventureMap.monsterThreat.levels.3"  : "Um pouco mais fraca",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "Um Pouco Mais Fraca",
 	"vcmi.adventureMap.monsterThreat.levels.4"  : "Igual",
-	"vcmi.adventureMap.monsterThreat.levels.5"  : "Um pouco mais forte",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "Um Pouco Mais Forte",
 	"vcmi.adventureMap.monsterThreat.levels.6"  : "Forte",
 	"vcmi.adventureMap.monsterThreat.levels.7"  : "Muito Forte",
 	"vcmi.adventureMap.monsterThreat.levels.8"  : "Desafiante",
@@ -163,8 +163,8 @@
 	"vcmi.systemOptions.townsGroup" : "Tela da Cidade",
 
 	"vcmi.statisticWindow.statistics" : "Estatísticas",
-	"vcmi.statisticWindow.tsvCopy" : "Copiar dados",
-	"vcmi.statisticWindow.selectView" : "Selecionar visualização",
+	"vcmi.statisticWindow.tsvCopy" : "Para a área de transf.",
+	"vcmi.statisticWindow.selectView" : "Selec. visualização",
 	"vcmi.statisticWindow.value" : "Valor",
 	"vcmi.statisticWindow.title.overview" : "Visão geral",
 	"vcmi.statisticWindow.title.resources" : "Recursos",
@@ -178,7 +178,7 @@
 	"vcmi.statisticWindow.title.experience" : "Experiência",
 	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Custo do exército",
 	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Custo de construção",
-	"vcmi.statisticWindow.title.mapExplored" : "Exploração do mapa",
+	"vcmi.statisticWindow.title.mapExplored" : "Mapa explorado",
 	"vcmi.statisticWindow.param.playerName" : "Nome do jogador",
 	"vcmi.statisticWindow.param.daysSurvived" : "Dias sobrevividos",
 	"vcmi.statisticWindow.param.maxHeroLevel" : "Nível máximo do herói",
@@ -325,8 +325,8 @@
 	"vcmi.townHall.missingBase"             : "A construção base %s deve ser construída primeiro",
 	"vcmi.townHall.noCreaturesToRecruit"    : "Não há criaturas para recrutar!",
 
-	"vcmi.townStructure.bank.borrow" : "Você entra no banco. Um banqueiro o vê e diz: \"Temos uma oferta especial para você. Você pode tomar um empréstimo de 2500 de ouro por 5 dias. Você terá que pagar 500 de ouro todos os dias.\"",
-	"vcmi.townStructure.bank.payBack" : "Você entra no banco. Um banqueiro o vê e diz: \"Você já pegou um empréstimo. Pague-o antes de tomar um novo.\"",
+	"vcmi.townStructure.bank.borrow" : "Você entra no banco. Um banqueiro o vê e diz: \"Temos uma oferta especial para você. Você pode pegar um empréstimo de 2500 de ouro por 5 dias. Você terá que pagar 500 de ouro todos os dias.\"",
+	"vcmi.townStructure.bank.payBack" : "Você entra no banco. Um banqueiro o vê e diz: \"Você já pegou um empréstimo. Pague-o antes de pegar um novo.\"",
 
 	"vcmi.logicalExpressions.anyOf"  : "Qualquer um dos seguintes:",
 	"vcmi.logicalExpressions.allOf"  : "Todos os seguintes:",
@@ -660,5 +660,7 @@
 	"core.bonus.WATER_IMMUNITY.name" : "Imunidade à Água",
 	"core.bonus.WATER_IMMUNITY.description" : "Imune a todos os feitiços da escola de magia da Água",
 	"core.bonus.WIDE_BREATH.name" : "Sopro Amplo",
-	"core.bonus.WIDE_BREATH.description" : "Ataque de sopro amplo (vários hexágonos)"
+	"core.bonus.WIDE_BREATH.description" : "Ataque de sopro amplo (vários hexágonos)",
+	"core.bonus.DISINTEGRATE.name": "Desintegrar",
+	"core.bonus.DISINTEGRATE.description": "Nenhum corpo permanece após a morte"
 }

+ 1 - 0
client/render/AssetGenerator.cpp

@@ -25,6 +25,7 @@ void AssetGenerator::generateAll()
 	createAdventureOptionsCleanBackground();
 	for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
 		createPlayerColoredBackground(PlayerColor(i));
+	createCombatUnitNumberWindow();
 }
 
 void AssetGenerator::createAdventureOptionsCleanBackground()

+ 2 - 2
client/render/Canvas.cpp

@@ -191,14 +191,14 @@ void Canvas::drawText(const Point & position, const EFonts & font, const ColorRG
 
 void Canvas::drawColor(const Rect & target, const ColorRGBA & color)
 {
-	Rect realTarget = (target + renderArea.topLeft()) * getScalingFactor();
+	Rect realTarget = target * getScalingFactor() + renderArea.topLeft();
 
 	CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color));
 }
 
 void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color)
 {
-	Rect realTarget = (target + renderArea.topLeft()) * getScalingFactor();
+	Rect realTarget = target * getScalingFactor() + renderArea.topLeft();
 
 	CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color));
 }

+ 4 - 0
client/renderSDL/SDLImage.cpp

@@ -415,6 +415,10 @@ void SDLImageIndexed::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove,
 
 void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
 {
+	// If shadow is enabled, following colors must be skipped unconditionally
+	if (shadowEnabled)
+		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
+
 	// Note: here we skip first colors in the palette that are predefined in H3 images
 	for(int i = 0; i < currentPalette->ncolors; i++)
 	{

+ 16 - 1
client/windows/CCastleInterface.cpp

@@ -54,6 +54,7 @@
 #include "../../lib/entities/building/CBuilding.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapObjects/TownBuildingInstance.h"
 
 
 static bool useCompactCreatureBox()
@@ -845,7 +846,21 @@ bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, Bu
 
 void CCastleBuildings::enterRewardable(BuildingID building)
 {
-	LOCPLINT->cb->visitTownBuilding(town, building);
+	if (town->visitingHero == nullptr)
+	{
+		MetaString message;
+		message.appendTextID("core.genrltxt.273"); // only visiting heroes may visit %s
+		message.replaceTextID(town->town->buildings.at(building)->getNameTextID());
+
+		LOCPLINT->showInfoDialog(message.toString());
+	}
+	else
+	{
+		if (town->rewardableBuildings.at(building)->wasVisited(town->visitingHero))
+			enterBuilding(building);
+		else
+			LOCPLINT->cb->visitTownBuilding(town, building);
+	}
 }
 
 void CCastleBuildings::enterBlacksmith(BuildingID building, ArtifactID artifactID)

+ 7 - 5
client/windows/CWindowObject.cpp

@@ -20,6 +20,7 @@
 #include "../windows/CMessage.h"
 #include "../renderSDL/SDL_PixelAccess.h"
 #include "../render/IImage.h"
+#include "../render/IScreenHandler.h"
 #include "../render/IRenderHandler.h"
 #include "../render/Canvas.h"
 
@@ -115,7 +116,8 @@ void CWindowObject::updateShadow()
 void CWindowObject::setShadow(bool on)
 {
 	//size of shadow
-	static const int size = 8;
+	int sizeOriginal = 8;
+	int size = sizeOriginal * GH.screenHandler().getScalingFactor();
 
 	if(on == !shadowParts.empty())
 		return;
@@ -180,9 +182,9 @@ void CWindowObject::setShadow(bool on)
 		//FIXME: do something with this points
 		Point shadowStart;
 		if (options & BORDERED)
-			shadowStart = Point(size - 14, size - 14);
+			shadowStart = Point(sizeOriginal - 14, sizeOriginal - 14);
 		else
-			shadowStart = Point(size, size);
+			shadowStart = Point(sizeOriginal, sizeOriginal);
 
 		Point shadowPos;
 		if (options & BORDERED)
@@ -198,8 +200,8 @@ void CWindowObject::setShadow(bool on)
 
 		//create base 8x8 piece of shadow
 		SDL_Surface * shadowCorner = CSDL_Ext::copySurface(shadowCornerTempl);
-		SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, fullsize.x - size, size);
-		SDL_Surface * shadowRight  = CSDL_Ext::scaleSurface(shadowRightTempl,  size, fullsize.y - size);
+		SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, (fullsize.x - sizeOriginal) * GH.screenHandler().getScalingFactor(), size);
+		SDL_Surface * shadowRight  = CSDL_Ext::scaleSurface(shadowRightTempl,  size, (fullsize.y - sizeOriginal) * GH.screenHandler().getScalingFactor());
 
 		blitAlphaCol(shadowBottom, 0);
 		blitAlphaRow(shadowRight, 0);

+ 1 - 2
conanfile.py

@@ -19,7 +19,7 @@ class VCMI(ConanFile):
         "sdl_image/[~2.0.5]",
         "sdl_mixer/[~2.0.4]",
         "sdl_ttf/[~2.0.18]",
-        "onetbb/[^2021.3]",
+        "onetbb/[^2021.7 <2021.10]",  # 2021.10+ breaks mobile builds due to added hwloc dependency
         "xz_utils/[>=5.2.5]", # Required for innoextract
     ]
 
@@ -39,7 +39,6 @@ class VCMI(ConanFile):
 
         "boost/*:shared": True,
         "minizip/*:shared": True,
-        "onetbb/*:shared": True,
     }
 
     def configure(self):

+ 1 - 0
docs/modders/Entities_Format/Town_Building_Format.md

@@ -197,6 +197,7 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
 	"bonuses" : [ BONUS_FORMAT ]
 	
 	// If set to true, this building will not automatically activate on new day or on entering town and needs to be activated manually on click
+	// Note that such building can only be activated by visiting hero, and not by garrisoned hero.
 	"manualHeroVisit" : false,
 	
 	// Bonuses provided by this special building if this building or any of its upgrades are constructed in town

+ 81 - 1
lib/json/JsonValidator.cpp

@@ -21,6 +21,80 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+// Algorithm for detection of typos in words
+// Determines how 'different' two strings are - how many changes must be done to turn one string into another one
+// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
+static int getLevenshteinDistance(const std::string & s, const std::string & t)
+{
+	int n = t.size();
+	int m = s.size();
+
+	// create two work vectors of integer distances
+	std::vector<int> v0(n+1, 0);
+	std::vector<int> v1(n+1, 0);
+
+	// initialize v0 (the previous row of distances)
+	// this row is A[0][i]: edit distance from an empty s to t;
+	// that distance is the number of characters to append to  s to make t.
+	for (int i = 0; i < n; ++i)
+		v0[i] = i;
+
+	for (int i = 0; i < m; ++i)
+	{
+		// calculate v1 (current row distances) from the previous row v0
+
+		// first element of v1 is A[i + 1][0]
+		// edit distance is delete (i + 1) chars from s to match empty t
+		v1[0] = i + 1;
+
+		// use formula to fill in the rest of the row
+		for (int j = 0; j < n; ++j)
+		{
+			// calculating costs for A[i + 1][j + 1]
+			int deletionCost = v0[j + 1] + 1;
+			int insertionCost = v1[j] + 1;
+			int substitutionCost;
+
+			if (s[i] == t[j])
+				substitutionCost = v0[j];
+			else
+				substitutionCost = v0[j] + 1;
+
+			v1[j + 1] = std::min({deletionCost, insertionCost, substitutionCost});
+		}
+
+		// copy v1 (current row) to v0 (previous row) for next iteration
+		// since data in v1 is always invalidated, a swap without copy could be more efficient
+		std::swap(v0, v1);
+	}
+
+	// after the last swap, the results of v1 are now in v0
+	return v0[n];
+}
+
+/// Searches for keys similar to 'target' in 'candidates' map
+/// Returns closest match or empty string if no suitable candidates are found
+static std::string findClosestMatch(JsonMap candidates, std::string target)
+{
+	// Maximum distance at which we can consider strings to be similar
+	// If strings have more different symbols than this number then it is not a typo, but a completely different word
+	static constexpr int maxDistance = 5;
+	int bestDistance = maxDistance;
+	std::string bestMatch;
+
+	for (auto const & candidate : candidates)
+	{
+		int newDistance = getLevenshteinDistance(candidate.first, target);
+
+		if (newDistance < bestDistance)
+		{
+			bestDistance = newDistance;
+			bestMatch = candidate.first;
+		}
+	}
+	return bestMatch;
+}
+
 static std::string emptyCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data)
 {
 	// check is not needed - e.g. incorporated into another check
@@ -417,7 +491,13 @@ static std::string additionalPropertiesCheck(JsonValidator & validator, const Js
 
 			// or, additionalItems field can be bool which indicates if such items are allowed
 			else if(!schema.isNull() && !schema.Bool()) // present and set to false - error
-				errors += validator.makeErrorMessage("Unknown entry found: " + entry.first);
+			{
+				std::string bestCandidate = findClosestMatch(baseSchema["properties"].Struct(), entry.first);
+				if (!bestCandidate.empty())
+					errors += validator.makeErrorMessage("Unknown entry found: '" + entry.first + "'. Perhaps you meant '" + bestCandidate + "'?");
+				else
+					errors += validator.makeErrorMessage("Unknown entry found: " + entry.first);
+			}
 		}
 	}
 	return errors;

+ 3 - 0
lib/mapObjects/CRewardableObject.cpp

@@ -246,6 +246,9 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int3
 	}
 	else
 	{
+		if (answer == 0)
+			return; //Player refused
+
 		if(answer > 0 && answer - 1 < configuration.info.size())
 		{
 			auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);

+ 5 - 0
lib/mapObjects/TownBuildingInstance.cpp

@@ -165,6 +165,11 @@ void TownRewardableBuildingInstance::grantReward(ui32 rewardID, const CGHeroInst
 	}
 }
 
+bool TownRewardableBuildingInstance::wasVisited(const CGHeroInstance * contextHero) const
+{
+	return wasVisitedBefore(contextHero);
+}
+
 bool TownRewardableBuildingInstance::wasVisitedBefore(const CGHeroInstance * contextHero) const
 {
 	switch (configuration.visitMode)

+ 1 - 0
lib/mapObjects/TownBuildingInstance.h

@@ -70,6 +70,7 @@ class DLL_LINKAGE TownRewardableBuildingInstance : public TownBuildingInstance,
 public:
 	void setProperty(ObjProperty what, ObjPropertyID identifier) override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
+	bool wasVisited(const CGHeroInstance * contextHero) const override;
 	
 	void newTurn(vstd::RNG & rand) const override;
 	

+ 1 - 0
mapeditor/CMakeLists.txt

@@ -196,6 +196,7 @@ endif()
 target_sources(vcmieditor PRIVATE
 	${editor_SRCS}
 	${editor_HEADERS}
+	${editor_FORMS}
 	${editor_RESOURCES}
 )
 

+ 12 - 5
mapeditor/inspector/towneventdialog.cpp

@@ -13,6 +13,7 @@
 #include "towneventdialog.h"
 #include "ui_towneventdialog.h"
 #include "mapeditorroles.h"
+#include "../mapsettings/eventsettings.h"
 #include "../../lib/entities/building/CBuilding.h"
 #include "../../lib/entities/faction/CTownHandler.h"
 #include "../../lib/constants/NumericConstants.h"
@@ -63,9 +64,10 @@ TownEventDialog::~TownEventDialog()
 
 void TownEventDialog::initPlayers()
 {
+	auto playerList = params.value("players").toList();
 	for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
 	{
-		bool isAffected = (1 << i) & params.value("players").toInt();
+		bool isAffected = playerList.contains(toQString(PlayerColor(i)));
 		auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i]));
 		item->setData(MapEditorRoles::PlayerIDRole, QVariant::fromValue(i));
 		item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked);
@@ -155,7 +157,12 @@ void TownEventDialog::initCreatures()
 {
 	auto creatures = params.value("creatures").toList();
 	auto * ctown = town.town;
-	for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; ++i)
+	if (!ctown)
+		ui->creaturesTable->setRowCount(GameConstants::CREATURES_PER_TOWN);
+	else 
+		ui->creaturesTable->setRowCount(ctown->creatures.size());
+
+	for (int i = 0; i < ui->creaturesTable->rowCount(); ++i)
 	{
 		QString creatureNames;
 		if (!ctown)
@@ -208,12 +215,12 @@ void TownEventDialog::on_TownEventDialog_finished(int result)
 
 QVariant TownEventDialog::playersToVariant()
 {
-	int players = 0;
+	QVariantList players;
 	for (int i = 0; i < ui->playersAffected->count(); ++i)
 	{
 		auto * item = ui->playersAffected->item(i);
 		if (item->checkState() == Qt::Checked)
-			players |= 1 << i;
+			players.push_back(toQString(PlayerColor(i)));
 	}
 	return QVariant::fromValue(players);
 }
@@ -239,7 +246,7 @@ QVariantList TownEventDialog::buildingsToVariant()
 QVariantList TownEventDialog::creaturesToVariant()
 {
 	QVariantList creaturesList;
-	for (int i = 0; i < GameConstants::CREATURES_PER_TOWN; ++i)
+	for (int i = 0; i < ui->creaturesTable->rowCount(); ++i)
 	{
 		auto * item = static_cast<QSpinBox *>(ui->creaturesTable->cellWidget(i, 1));
 		creaturesList.push_back(item->value());

+ 18 - 25
mapeditor/jsonutils.cpp

@@ -17,7 +17,7 @@ static QVariantMap JsonToMap(const JsonMap & json)
 	QVariantMap map;
 	for(auto & entry : json)
 	{
-		map.insert(QString::fromUtf8(entry.first.c_str()), JsonUtils::toVariant(entry.second));
+		map.insert(QString::fromStdString(entry.first), JsonUtils::toVariant(entry.second));
 	}
 	return map;
 }
@@ -61,23 +61,18 @@ QVariant toVariant(const JsonNode & node)
 {
 	switch(node.getType())
 	{
-		break;
 	case JsonNode::JsonType::DATA_NULL:
 		return QVariant();
-		break;
 	case JsonNode::JsonType::DATA_BOOL:
 		return QVariant(node.Bool());
-		break;
 	case JsonNode::JsonType::DATA_FLOAT:
-	case JsonNode::JsonType::DATA_INTEGER:
 		return QVariant(node.Float());
-		break;
+	case JsonNode::JsonType::DATA_INTEGER:
+		return QVariant{static_cast<qlonglong>(node.Integer())};
 	case JsonNode::JsonType::DATA_STRING:
-		return QVariant(QString::fromUtf8(node.String().c_str()));
-		break;
+		return QVariant(QString::fromStdString(node.String()));
 	case JsonNode::JsonType::DATA_VECTOR:
 		return JsonToList(node.Vector());
-		break;
 	case JsonNode::JsonType::DATA_STRUCT:
 		return JsonToMap(node.Struct());
 	}
@@ -87,33 +82,31 @@ QVariant toVariant(const JsonNode & node)
 QVariant JsonFromFile(QString filename)
 {
 	QFile file(filename);
-	file.open(QFile::ReadOnly);
-	auto data = file.readAll();
-
-	if(data.size() == 0)
+	if(!file.open(QFile::ReadOnly))
 	{
-		logGlobal->error("Failed to open file %s", filename.toUtf8().data());
-		return QVariant();
-	}
-	else
-	{
-		JsonNode node(reinterpret_cast<const std::byte*>(data.data()), data.size(), filename.toStdString());
-		return toVariant(node);
+		logGlobal->error("Failed to open file %s. Reason: %s", qUtf8Printable(filename), qUtf8Printable(file.errorString()));
+		return {};
 	}
+
+	const auto data = file.readAll();
+	JsonNode node(reinterpret_cast<const std::byte*>(data.data()), data.size(), filename.toStdString());
+	return toVariant(node);
 }
 
 JsonNode toJson(QVariant object)
 {
 	JsonNode ret;
 
-	if(object.canConvert<QVariantMap>())
-		ret.Struct() = VariantToMap(object.toMap());
-	else if(object.canConvert<QVariantList>())
-		ret.Vector() = VariantToList(object.toList());
-	else if(object.userType() == QMetaType::QString)
+	if(object.userType() == QMetaType::QString)
 		ret.String() = object.toString().toUtf8().data();
 	else if(object.userType() == QMetaType::Bool)
 		ret.Bool() = object.toBool();
+	else if(object.canConvert<QVariantMap>())
+		ret.Struct() = VariantToMap(object.toMap());
+	else if(object.canConvert<QVariantList>())
+		ret.Vector() = VariantToList(object.toList());
+	else if(object.canConvert<int>())
+		ret.Integer() = object.toInt();
 	else if(object.canConvert<double>())
 		ret.Float() = object.toFloat();
 

+ 6 - 1
mapeditor/mapsettings/eventsettings.cpp

@@ -16,11 +16,16 @@
 #include "../../lib/constants/NumericConstants.h"
 #include "../../lib/constants/StringConstants.h"
 
+QString toQString(const PlayerColor & player)
+{
+	return QString::fromStdString(player.toString());
+}
+
 QVariant toVariant(const std::set<PlayerColor> & players)
 {
 	QVariantList result;
 	for(auto const id : players)
-		result.push_back(QString::fromStdString(id.toString()));
+		result.push_back(toQString(id));
 	return result;
 }
 

+ 1 - 0
mapeditor/mapsettings/eventsettings.h

@@ -15,6 +15,7 @@ namespace Ui {
 class EventSettings;
 }
 
+QString toQString(const PlayerColor & player);
 QVariant toVariant(const TResources & resources);
 QVariant toVariant(const std::set<PlayerColor> & players);
 

+ 5 - 3
mapeditor/mapsettings/timedevent.cpp

@@ -10,6 +10,7 @@
 #include "StdInc.h"
 #include "timedevent.h"
 #include "ui_timedevent.h"
+#include "eventsettings.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 #include "../../lib/constants/StringConstants.h"
 
@@ -30,9 +31,10 @@ TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) :
 	ui->eventFirstOccurrence->setValue(params.value("firstOccurrence").toInt());
 	ui->eventRepeatAfter->setValue(params.value("nextOccurrence").toInt());
 
+	auto playerList = params.value("players").toList();
 	for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
 	{
-		bool isAffected = (1 << i) & params.value("players").toInt();
+		bool isAffected = playerList.contains(toQString(PlayerColor(i)));
 		auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i]));
 		item->setData(Qt::UserRole, QVariant::fromValue(i));
 		item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked);
@@ -69,12 +71,12 @@ void TimedEvent::on_TimedEvent_finished(int result)
 	descriptor["firstOccurrence"] = QVariant::fromValue(ui->eventFirstOccurrence->value());
 	descriptor["nextOccurrence"] = QVariant::fromValue(ui->eventRepeatAfter->value());
 
-	int players = 0;
+	QVariantList players;
 	for(int i = 0; i < ui->playersAffected->count(); ++i)
 	{
 		auto * item = ui->playersAffected->item(i);
 		if(item->checkState() == Qt::Checked)
-			players |= 1 << i;
+			players.push_back(toQString(PlayerColor(i)));
 	}
 	descriptor["players"] = QVariant::fromValue(players);
 

+ 10 - 10
mapeditor/translation/portuguese.ts

@@ -67,22 +67,22 @@
     <message>
         <location filename="../mapsettings/generalsettings.ui" line="52"/>
         <source>Author</source>
-        <translation type="unfinished"></translation>
+        <translation>Autor</translation>
     </message>
     <message>
         <location filename="../mapsettings/generalsettings.ui" line="62"/>
         <source>Author contact (e.g. email)</source>
-        <translation type="unfinished"></translation>
+        <translation>Contato do autor (ex.: e-mail)</translation>
     </message>
     <message>
         <location filename="../mapsettings/generalsettings.ui" line="72"/>
         <source>Map Creation Time</source>
-        <translation type="unfinished"></translation>
+        <translation>Data de Criação do Mapa</translation>
     </message>
     <message>
         <location filename="../mapsettings/generalsettings.ui" line="86"/>
         <source>Map Version</source>
-        <translation type="unfinished"></translation>
+        <translation>Versão do Mapa</translation>
     </message>
     <message>
         <location filename="../mapsettings/generalsettings.ui" line="120"/>
@@ -947,17 +947,17 @@
     <message>
         <location filename="../mapcontroller.cpp" line="405"/>
         <source>Can&apos;t place object</source>
-        <translation type="unfinished">Não é possível colocar objeto</translation>
+        <translation>Não é possível colocar objeto</translation>
     </message>
     <message>
         <location filename="../mapcontroller.cpp" line="577"/>
         <source>There can only be one grail object on the map.</source>
-        <translation type="unfinished"></translation>
+        <translation>Pode haver apenas um objeto graal no mapa.</translation>
     </message>
     <message>
         <location filename="../mapcontroller.cpp" line="583"/>
         <source>Hero %1 cannot be created as NEUTRAL.</source>
-        <translation type="unfinished"></translation>
+        <translation>O herói %1 não pode ser criado como NEUTRO.</translation>
     </message>
 </context>
 <context>
@@ -1762,17 +1762,17 @@
     <message>
         <location filename="../validator.cpp" line="148"/>
         <source>Spell scroll %1 doesn&apos;t have instance assigned and must be removed</source>
-        <translation type="unfinished"></translation>
+        <translation>O pergaminho de feitiço %1 não tem uma instância atribuída e deve ser removido</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="154"/>
         <source>Artifact %1 is prohibited by map settings</source>
-        <translation type="unfinished"></translation>
+        <translation>O artefato %1 é proibido pelas configurações do mapa</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="168"/>
         <source>Player %1 has no towns and heroes assigned</source>
-        <translation type="unfinished"></translation>
+        <translation>O jogador %1 não tem cidades e heróis atribuídos</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="116"/>

+ 8 - 0
mapeditor/windownewmap.cpp

@@ -150,6 +150,10 @@ bool WindowNewMap::loadUserSettings()
 				ui->monsterOpt4->setChecked(true); break;
 		}
 
+		ui->roadDirt->setChecked(mapGenOptions.isRoadEnabled(Road::DIRT_ROAD));
+		ui->roadGravel->setChecked(mapGenOptions.isRoadEnabled(Road::GRAVEL_ROAD));
+		ui->roadCobblestone->setChecked(mapGenOptions.isRoadEnabled(Road::COBBLESTONE_ROAD));
+
 		ret = true;
 	}
 
@@ -236,6 +240,10 @@ void WindowNewMap::on_okButton_clicked()
 
 	mapGenOptions.setWaterContent(water);
 	mapGenOptions.setMonsterStrength(monster);
+
+	mapGenOptions.setRoadEnabled(Road::DIRT_ROAD, ui->roadDirt->isChecked());
+	mapGenOptions.setRoadEnabled(Road::GRAVEL_ROAD, ui->roadGravel->isChecked());
+	mapGenOptions.setRoadEnabled(Road::COBBLESTONE_ROAD, ui->roadCobblestone->isChecked());
 	
 	saveUserSettings();
 

+ 136 - 39
mapeditor/windownewmap.ui

@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>444</width>
-    <height>445</height>
+    <height>506</height>
    </rect>
   </property>
   <property name="sizePolicy">
@@ -52,7 +52,7 @@
       <x>0</x>
       <y>20</y>
       <width>281</width>
-      <height>68</height>
+      <height>73</height>
      </rect>
     </property>
     <layout class="QGridLayout" name="gridLayout_2" columnstretch="3,0,1">
@@ -72,7 +72,7 @@
         </size>
        </property>
        <property name="inputMethodHints">
-        <set>Qt::ImhDigitsOnly</set>
+        <set>Qt::InputMethodHint::ImhDigitsOnly</set>
        </property>
        <property name="text">
         <string notr="true">36</string>
@@ -98,7 +98,7 @@
         </size>
        </property>
        <property name="inputMethodHints">
-        <set>Qt::ImhDigitsOnly</set>
+        <set>Qt::InputMethodHint::ImhDigitsOnly</set>
        </property>
        <property name="text">
         <string notr="true">36</string>
@@ -132,10 +132,10 @@
        <item>
         <spacer name="horizontalSpacer_5">
          <property name="orientation">
-          <enum>Qt::Horizontal</enum>
+          <enum>Qt::Orientation::Horizontal</enum>
          </property>
          <property name="sizeType">
-          <enum>QSizePolicy::Fixed</enum>
+          <enum>QSizePolicy::Policy::Fixed</enum>
          </property>
          <property name="sizeHint" stdset="0">
           <size>
@@ -207,7 +207,7 @@
      <x>10</x>
      <y>140</y>
      <width>431</width>
-     <height>301</height>
+     <height>361</height>
     </rect>
    </property>
    <property name="sizePolicy">
@@ -237,7 +237,7 @@
        <x>10</x>
        <y>20</y>
        <width>391</width>
-       <height>68</height>
+       <height>72</height>
       </rect>
      </property>
      <layout class="QGridLayout" name="gridLayout" columnstretch="0,0,0,0">
@@ -546,7 +546,7 @@
          </size>
         </property>
         <property name="orientation">
-         <enum>Qt::Horizontal</enum>
+         <enum>Qt::Orientation::Horizontal</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
@@ -675,7 +675,7 @@
       <item>
        <spacer name="horizontalSpacer">
         <property name="orientation">
-         <enum>Qt::Horizontal</enum>
+         <enum>Qt::Orientation::Horizontal</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
@@ -688,13 +688,110 @@
      </layout>
     </widget>
    </widget>
-   <widget class="QWidget" name="layoutWidget5">
+   <widget class="QGroupBox" name="groupBox_6">
     <property name="geometry">
      <rect>
       <x>10</x>
       <y>230</y>
       <width>411</width>
-      <height>32</height>
+      <height>51</height>
+     </rect>
+    </property>
+    <property name="sizePolicy">
+     <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+      <horstretch>0</horstretch>
+      <verstretch>0</verstretch>
+     </sizepolicy>
+    </property>
+    <property name="maximumSize">
+     <size>
+      <width>480</width>
+      <height>96</height>
+     </size>
+    </property>
+    <property name="title">
+     <string>Roads</string>
+    </property>
+    <widget class="QWidget" name="layoutWidget4_2">
+     <property name="geometry">
+      <rect>
+       <x>0</x>
+       <y>20</y>
+       <width>411</width>
+       <height>26</height>
+      </rect>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="0,0,0,0,0,1">
+      <item>
+       <widget class="QCheckBox" name="roadDirt">
+        <property name="text">
+         <string>Dirt</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer_4">
+        <property name="orientation">
+         <enum>Qt::Orientation::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QCheckBox" name="roadGravel">
+        <property name="text">
+         <string>Gravel</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer_6">
+        <property name="orientation">
+         <enum>Qt::Orientation::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QCheckBox" name="roadCobblestone">
+        <property name="text">
+         <string>Cobblestone</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer_3">
+        <property name="orientation">
+         <enum>Qt::Orientation::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </widget>
+   <widget class="QWidget" name="layoutWidget5">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>280</y>
+      <width>411</width>
+      <height>34</height>
      </rect>
     </property>
     <layout class="QHBoxLayout" name="horizontalLayout_3">
@@ -732,37 +829,37 @@
      </item>
     </layout>
    </widget>
-   <widget class="QLineEdit" name="lineSeed">
-    <property name="enabled">
-     <bool>false</bool>
-    </property>
+   <widget class="QWidget" name="layoutWidget">
     <property name="geometry">
      <rect>
-      <x>280</x>
-      <y>270</y>
-      <width>131</width>
-      <height>21</height>
+      <x>80</x>
+      <y>320</y>
+      <width>283</width>
+      <height>33</height>
      </rect>
     </property>
-    <property name="inputMethodHints">
-     <set>Qt::ImhDigitsOnly</set>
-    </property>
-    <property name="text">
-     <string>0</string>
-    </property>
-   </widget>
-   <widget class="QCheckBox" name="checkSeed">
-    <property name="geometry">
-     <rect>
-      <x>110</x>
-      <y>270</y>
-      <width>161</width>
-      <height>20</height>
-     </rect>
-    </property>
-    <property name="text">
-     <string>Custom seed</string>
-    </property>
+    <layout class="QHBoxLayout" name="horizontalLayout_6">
+     <item>
+      <widget class="QCheckBox" name="checkSeed">
+       <property name="text">
+        <string>Custom seed</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="lineSeed">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="inputMethodHints">
+        <set>Qt::InputMethodHint::ImhDigitsOnly</set>
+       </property>
+       <property name="text">
+        <string>0</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
    </widget>
   </widget>
   <widget class="QCheckBox" name="randomMapCheck">

+ 35 - 18
server/CGameHandler.cpp

@@ -1171,7 +1171,6 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta
 		sendAndApply(&vc);
 	}
 	visitCastleObjects(obj, hero);
-	giveSpells (obj, hero);
 
 	if (obj->visitingHero && obj->garrisonHero)
 		useScholarSkill(obj->visitingHero->id, obj->garrisonHero->id);
@@ -1180,10 +1179,27 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta
 
 void CGameHandler::visitCastleObjects(const CGTownInstance * t, const CGHeroInstance * h)
 {
+	std::vector<const CGHeroInstance * > visitors;
+	visitors.push_back(h);
+	visitCastleObjects(t, visitors);
+}
+
+void CGameHandler::visitCastleObjects(const CGTownInstance * t, std::vector<const CGHeroInstance * > visitors)
+{
+	std::vector<BuildingID> buildingsToVisit;
+	for (auto const & hero : visitors)
+		giveSpells (t, hero);
+
 	for (auto & building : t->rewardableBuildings)
 	{
 		if (!t->town->buildings.at(building.first)->manualHeroVisit)
-			building.second->onHeroVisit(h);
+			buildingsToVisit.push_back(building.first);
+	}
+
+	if (!buildingsToVisit.empty())
+	{
+		auto visitQuery = std::make_shared<TownBuildingVisitQuery>(this, t, visitors, buildingsToVisit);
+		queries->addQuery(visitQuery);
 	}
 }
 
@@ -2144,10 +2160,15 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID,
 
 	if (!force)
 	{
-		if(t->garrisonHero) //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
-			objectVisited(t, t->garrisonHero);
-		if(t->visitingHero)
-			objectVisited(t, t->visitingHero);
+		//garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
+		std::vector<const CGHeroInstance *> visitors;
+		if (t->garrisonHero)
+			visitors.push_back(t->garrisonHero);
+		if (t->visitingHero)
+			visitors.push_back(t->visitingHero);
+
+		if (!visitors.empty())
+			visitCastleObjects(t, visitors);
 	}
 
 	checkVictoryLossConditionsForPlayer(t->tempOwner);
@@ -2173,19 +2194,15 @@ bool CGameHandler::visitTownBuilding(ObjectInstanceID tid, BuildingID bid)
 		return true;
 	}
 
-	if (t->rewardableBuildings.count(bid))
+	if (t->rewardableBuildings.count(bid) && t->visitingHero && t->town->buildings.at(bid)->manualHeroVisit)
 	{
-		auto & hero = t->garrisonHero ? t->garrisonHero : t->visitingHero;
-		auto * building = t->rewardableBuildings.at(bid);
-
-		if (hero && t->town->buildings.at(bid)->manualHeroVisit)
-		{
-			auto visitQuery = std::make_shared<TownBuildingVisitQuery>(this, t, hero, bid);
-			queries->addQuery(visitQuery);
-			building->onHeroVisit(hero);
-			queries->popIfTop(visitQuery);
-			return true;
-		}
+		std::vector<BuildingID> buildingsToVisit;
+		std::vector<const CGHeroInstance*> visitors;
+		buildingsToVisit.push_back(bid);
+		visitors.push_back(t->visitingHero);
+		auto visitQuery = std::make_shared<TownBuildingVisitQuery>(this, t, visitors, buildingsToVisit);
+		queries->addQuery(visitQuery);
+		return true;
 	}
 
 	return true;

+ 1 - 0
server/CGameHandler.h

@@ -182,6 +182,7 @@ public:
 	void visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h);
 	bool teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui8 source, PlayerColor asker = PlayerColor::NEUTRAL);
 	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override;
+	void visitCastleObjects(const CGTownInstance * obj, std::vector<const CGHeroInstance * > visitors);
 	void levelUpHero(const CGHeroInstance * hero, SecondarySkill skill);//handle client respond and send one more request if needed
 	void levelUpHero(const CGHeroInstance * hero);//initial call - check if hero have remaining levelups & handle them
 	void levelUpCommander (const CCommanderInstance * c, int skill); //secondary skill 1 to 6, special skill : skill - 100

+ 1 - 1
server/processors/NewTurnProcessor.cpp

@@ -110,7 +110,7 @@ void NewTurnProcessor::handleTownEvents(const CGTownInstance * town)
 
 			for (si32 i=0;i<event.creatures.size();i++) //creature growths
 			{
-				if (!town->creatures.at(i).second.empty() && event.creatures.at(i) > 0)//there is dwelling
+				if (i < town->creatures.size() && !town->creatures.at(i).second.empty() && event.creatures.at(i) > 0)//there is dwelling
 				{
 					sac.creatures[i].first += event.creatures.at(i);
 					iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), event.creatures.at(i));

+ 25 - 5
server/queries/VisitQueries.cpp

@@ -11,6 +11,8 @@
 #include "VisitQueries.h"
 
 #include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapObjects/TownBuildingInstance.h"
 #include "../CGameHandler.h"
 #include "QueriesProcessor.h"
 
@@ -29,7 +31,7 @@ bool VisitQuery::blocksPack(const CPack * pack) const
 	return true;
 }
 
-void VisitQuery::onExposure(QueryPtr topQuery)
+void MapObjectVisitQuery::onExposure(QueryPtr topQuery)
 {
 	//Object may have been removed and deleted.
 	if(gh->isValidObject(visitedObject))
@@ -54,13 +56,31 @@ void MapObjectVisitQuery::onRemoval(PlayerColor color)
 		gh->removeObject(visitedObject, color);
 }
 
-TownBuildingVisitQuery::TownBuildingVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, BuildingID buildingToVisit)
-	: VisitQuery(owner, Obj, Hero)
-	, visitedBuilding(buildingToVisit)
+TownBuildingVisitQuery::TownBuildingVisitQuery(CGameHandler * owner, const CGTownInstance * Obj, std::vector<const CGHeroInstance *> heroes, std::vector<BuildingID> buildingToVisit)
+	: VisitQuery(owner, Obj, heroes.front())
+	, visitedTown(Obj)
+{
+	// generate in reverse order - first building-hero pair to handle must be in the end of vector
+	for (auto const * hero : boost::adaptors::reverse(heroes))
+		for (auto const & building : boost::adaptors::reverse(buildingToVisit))
+			visitedBuilding.push_back({ hero, building});
+}
+
+void TownBuildingVisitQuery::onExposure(QueryPtr topQuery)
 {
+	onAdded(players.front());
 }
 
-void TownBuildingVisitQuery::onRemoval(PlayerColor color)
+void TownBuildingVisitQuery::onAdded(PlayerColor color)
 {
+	while (!visitedBuilding.empty() && owner->topQuery(color).get() == this)
+	{
+		visitingHero = visitedBuilding.back().hero;
+		auto * building = visitedTown->rewardableBuildings.at(visitedBuilding.back().building);
+		building->onHeroVisit(visitingHero);
+		visitedBuilding.pop_back();
+	}
 
+	if (visitedBuilding.empty() && owner->topQuery(color).get() == this)
+		owner->popIfTop(*this);
 }

+ 22 - 10
server/queries/VisitQueries.h

@@ -11,19 +11,22 @@
 
 #include "CQuery.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+class CGTownInstance;
+VCMI_LIB_NAMESPACE_END
+
 //Created when hero visits object.
 //Removed when query above is resolved (or immediately after visit if no queries were created)
 class VisitQuery : public CQuery
 {
 protected:
-	VisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero);
+	VisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero);
 
 public:
-	const CGObjectInstance *visitedObject;
-	const CGHeroInstance *visitingHero;
+	const CGObjectInstance * visitedObject;
+	const CGHeroInstance * visitingHero;
 
-	bool blocksPack(const CPack *pack) const final;
-	void onExposure(QueryPtr topQuery) final;
+	bool blocksPack(const CPack * pack) const final;
 };
 
 class MapObjectVisitQuery final : public VisitQuery
@@ -31,17 +34,26 @@ class MapObjectVisitQuery final : public VisitQuery
 public:
 	bool removeObjectAfterVisit;
 
-	MapObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero);
+	MapObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero);
 
 	void onRemoval(PlayerColor color) final;
+	void onExposure(QueryPtr topQuery) final;
 };
 
 class TownBuildingVisitQuery final : public VisitQuery
 {
-public:
-	BuildingID visitedBuilding;
+	struct BuildingVisit
+	{
+		const CGHeroInstance * hero;
+		BuildingID building;
+	};
 
-	TownBuildingVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, BuildingID buildingToVisit);
+	const CGTownInstance * visitedTown;
+	std::vector<BuildingVisit> visitedBuilding;
 
-	void onRemoval(PlayerColor color) final;
+public:
+	TownBuildingVisitQuery(CGameHandler * owner, const CGTownInstance * Obj, std::vector<const CGHeroInstance *> heroes, std::vector<BuildingID> buildingToVisit);
+
+	void onAdded(PlayerColor color) final;
+	void onExposure(QueryPtr topQuery) final;
 };