Sfoglia il codice sorgente

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

Xilmi 1 anno fa
parent
commit
6a98f116ce

+ 10 - 0
Mods/vcmi/config/vcmi/chinese.json

@@ -72,6 +72,11 @@
 	"vcmi.lobby.noUnderground" : "无地下部分",
 	"vcmi.lobby.sortDate" : "以修改时间排序地图",
 	"vcmi.lobby.backToLobby" : "返回大厅",
+	"vcmi.lobby.author" : "作者",
+	"vcmi.lobby.handicap" : "障碍",
+	"vcmi.lobby.handicap.resource" : "给予玩家起始资源以外的更多资源,允许负值,但总量不会低于0(玩家永远不会能以负资源开始游戏)。",
+	"vcmi.lobby.handicap.income" : "按百分比改变玩家的各种收入,向上取整。",
+	"vcmi.lobby.handicap.growth" : "改变玩家拥有的城镇的生物增长率,向上取整。",
 
 	"vcmi.lobby.login.title" : "VCMI大厅",
 	"vcmi.lobby.login.username" : "用户名:",
@@ -237,6 +242,8 @@
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。",
 	"vcmi.battleOptions.endWithAutocombat.hover": "结束战斗",
 	"vcmi.battleOptions.endWithAutocombat.help": "{结束战斗}\n\n以自动战斗立即结束剩余战斗过程",
+	"vcmi.battleOptions.showQuickSpell.hover": "展示快捷法术面板",
+	"vcmi.battleOptions.showQuickSpell.help": "{展示快捷法术面板}\n\n展示快捷选择法术的面板。",
 
 	"vcmi.adventureMap.revisitObject.hover" : "重新访问",
 	"vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。",
@@ -286,6 +293,9 @@
 	"vcmi.townHall.missingBase"             : "必须先建造基础建筑 %s",
 	"vcmi.townHall.noCreaturesToRecruit"    : "没有可供招募的生物。",
 
+	"vcmi.townStructure.bank.borrow" : "你走进银行。一位银行职员看到你,说道:“我们为您提供了一个特别优惠。您可以向我们借2500金币,期限为5天。您每天需要偿还500金币。”",
+	"vcmi.townStructure.bank.payBack" : "你走进银行。一位银行职员看到你,说道:“您已经获得了贷款。还清之前,不能再申请新的贷款。”",
+
 	"vcmi.logicalExpressions.anyOf"  : "以下任一前提:",
 	"vcmi.logicalExpressions.allOf"  : "以下所有前提:",
 	"vcmi.logicalExpressions.noneOf" : "与此建筑冲突:",

+ 1 - 1
client/CPlayerInterface.cpp

@@ -1013,7 +1013,7 @@ void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector
 	}
 	std::shared_ptr<CInfoWindow> temp = CInfoWindow::create(text, playerID, components);
 
-	if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this)
+	if ((makingTurn || (battleInt && battleInt->curInt && battleInt->curInt.get() == this)) && GH.windows().count() > 0 && LOCPLINT == this)
 	{
 		CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
 		showingDialog->setBusy();

+ 2 - 2
client/lobby/CBonusSelection.cpp

@@ -86,14 +86,14 @@ CBonusSelection::CBonusSelection()
 	buttonVideo = std::make_shared<CButton>(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), playVideo, EShortcut::LOBBY_REPLAY_VIDEO);
 	buttonBack = std::make_shared<CButton>(Point(624, 536), AnimationPath::builtin("CBCANCB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL);
 
-	campaignName = std::make_shared<CLabel>(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName());
+	campaignName = std::make_shared<CLabel>(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName(), 250);
 
 	iconsMapSizes = std::make_shared<CAnimImage>(AnimationPath::builtin("SCNRMPSZ"), 4, 0, 735, 26);
 
 	labelCampaignDescription = std::make_shared<CLabel>(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]);
 	campaignDescription = std::make_shared<CTextBox>(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1);
 
-	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated());
+	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), 285);
 	labelMapDescription = std::make_shared<CLabel>(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]);
 	mapDescription = std::make_shared<CTextBox>("", Rect(480, 278, 292, 108), 1);
 

+ 1 - 1
client/lobby/CSelectionBase.cpp

@@ -135,7 +135,7 @@ InfoCard::InfoCard()
 
 	labelSaveDate = std::make_shared<CLabel>(310, 38, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE);
 	labelMapSize = std::make_shared<CLabel>(333, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE);
-	mapName = std::make_shared<CLabel>(26, 39, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, "", 285);
+	mapName = std::make_shared<CLabel>(26, 39, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, "", SEL->screenType == ESelectionScreen::campaignList ? 325 : 285);
 	Rect descriptionRect(26, 149, 320, 115);
 	mapDescription = std::make_shared<CTextBox>("", descriptionRect, 1);
 	playerListBg = std::make_shared<CPicture>(ImagePath::builtin("CHATPLUG.bmp"), 16, 276);

+ 2 - 1
client/windows/CCastleInterface.cpp

@@ -1894,8 +1894,9 @@ const CBuilding * CFortScreen::RecruitArea::getMyBuilding()
 	{
 		if (town->hasBuilt(myID))
 			build = town->town->buildings.at(myID);
-		myID.advance(town->town->creatures.size());
+		BuildingID::advanceDwelling(myID);
 	}
+
 	return build;
 }
 

+ 8 - 0
lib/constants/EntityIdentifiers.h

@@ -352,6 +352,14 @@ public:
 		return (dwelling - DWELL_FIRST) / (GameConstants::CREATURES_PER_TOWN - 1);
 	}
 
+	static void advanceDwelling(BuildingIDBase & dwelling)
+	{
+		if(dwelling != BuildingIDBase::DWELL_LVL_8)
+			dwelling.advance(GameConstants::CREATURES_PER_TOWN - 1);
+		else
+			dwelling.advance(1);
+	}
+
 	bool IsSpecialOrGrail() const
 	{
 		return num == SPECIAL_1 || num == SPECIAL_2 || num == SPECIAL_3 || num == SPECIAL_4 || num == GRAIL;

+ 71 - 68
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -120,7 +120,7 @@ std::vector<JsonNode> CObjectClassesHandler::loadLegacyData()
 		legacyTemplates.insert(std::make_pair(key, tmpl));
 	}
 
-	objects.resize(256);
+	mapObjectTypes.resize(256);
 
 	std::vector<JsonNode> ret(dataSize);// create storage for 256 objects
 	assert(dataSize == 256);
@@ -162,39 +162,39 @@ std::vector<JsonNode> CObjectClassesHandler::loadLegacyData()
 	return ret;
 }
 
-void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj)
+void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * baseObject)
 {
-	auto object = loadSubObjectFromJson(scope, identifier, entry, obj, obj->objects.size());
+	auto subObject = loadSubObjectFromJson(scope, identifier, entry, baseObject, baseObject->objectTypeHandlers.size());
 
-	assert(object);
-	obj->objects.push_back(object);
+	assert(subObject);
+	baseObject->objectTypeHandlers.push_back(subObject);
 
-	registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype);
+	registerObject(scope, baseObject->getJsonKey(), subObject->getSubTypeName(), subObject->subtype);
 	for(const auto & compatID : entry["compatibilityIdentifiers"].Vector())
-		registerObject(scope, obj->getJsonKey(), compatID.String(), object->subtype);
+		registerObject(scope, baseObject->getJsonKey(), compatID.String(), subObject->subtype);
 }
 
-void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index)
+void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * baseObject, size_t index)
 {
-	auto object = loadSubObjectFromJson(scope, identifier, entry, obj, index);
+	auto subObject = loadSubObjectFromJson(scope, identifier, entry, baseObject, index);
 
-	assert(object);
-	if (obj->objects.at(index) != nullptr)
+	assert(subObject);
+	if (baseObject->objectTypeHandlers.at(index) != nullptr)
 		throw std::runtime_error("Attempt to load already loaded object:" + identifier);
 
-	obj->objects.at(index) = object;
+	baseObject->objectTypeHandlers.at(index) = subObject;
 
-	registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype);
+	registerObject(scope, baseObject->getJsonKey(), subObject->getSubTypeName(), subObject->subtype);
 	for(const auto & compatID : entry["compatibilityIdentifiers"].Vector())
-		registerObject(scope, obj->getJsonKey(), compatID.String(), object->subtype);
+		registerObject(scope, baseObject->getJsonKey(), compatID.String(), subObject->subtype);
 }
 
-TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index)
+TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * baseObject, size_t index)
 {
 	assert(identifier.find(':') == std::string::npos);
 	assert(!scope.empty());
 
-	std::string handler = obj->handlerName;
+	std::string handler = baseObject->handlerName;
 	if(!handlerConstructors.count(handler))
 	{
 		logMod->error("Handler with name %s was not found!", handler);
@@ -206,10 +206,10 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin
 	auto createdObject = handlerConstructors.at(handler)();
 
 	createdObject->modScope = scope;
-	createdObject->typeName = obj->identifier;
+	createdObject->typeName = baseObject->identifier;
 	createdObject->subTypeName = identifier;
 
-	createdObject->type = obj->id;
+	createdObject->type = baseObject->id;
 	createdObject->subtype = index;
 	createdObject->init(entry);
 
@@ -223,7 +223,7 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin
 		}
 	}
 
-	auto range = legacyTemplates.equal_range(std::make_pair(obj->id, index));
+	auto range = legacyTemplates.equal_range(std::make_pair(baseObject->id, index));
 	for (auto & templ : boost::make_iterator_range(range.first, range.second))
 	{
 		if (staticObject)
@@ -238,7 +238,7 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin
 	}
 	legacyTemplates.erase(range.first, range.second);
 
-	logGlobal->debug("Loaded object %s(%d)::%s(%d)", obj->getJsonKey(), obj->id, identifier, index);
+	logGlobal->debug("Loaded object %s(%d)::%s(%d)", baseObject->getJsonKey(), baseObject->id, identifier, index);
 
 	return createdObject;
 }
@@ -263,17 +263,17 @@ std::string ObjectClass::getNameTranslated() const
 
 std::unique_ptr<ObjectClass> CObjectClassesHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index)
 {
-	auto obj = std::make_unique<ObjectClass>();
+	auto newObject = std::make_unique<ObjectClass>();
 
-	obj->modScope = scope;
-	obj->identifier = name;
-	obj->handlerName = json["handler"].String();
-	obj->base = json["base"];
-	obj->id = index;
+	newObject->modScope = scope;
+	newObject->identifier = name;
+	newObject->handlerName = json["handler"].String();
+	newObject->base = json["base"];
+	newObject->id = index;
 
-	VLC->generaltexth->registerString(scope, obj->getNameTextID(), json["name"].String());
+	VLC->generaltexth->registerString(scope, newObject->getNameTextID(), json["name"].String());
 
-	obj->objects.resize(json["lastReservedIndex"].Float() + 1);
+	newObject->objectTypeHandlers.resize(json["lastReservedIndex"].Float() + 1);
 
 	for (auto subData : json["types"].Struct())
 	{
@@ -284,68 +284,71 @@ std::unique_ptr<ObjectClass> CObjectClassesHandler::loadFromJson(const std::stri
 			if ( subMeta == "core")
 			{
 				size_t subIndex = subData.second["index"].Integer();
-				loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get(), subIndex);
+				loadSubObject(subData.second.getModScope(), subData.first, subData.second, newObject.get(), subIndex);
 			}
 			else
 			{
 				logMod->error("Object %s:%s.%s - attempt to load object with preset index! This option is reserved for built-in mod", subMeta, name, subData.first );
-				loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get());
+				loadSubObject(subData.second.getModScope(), subData.first, subData.second, newObject.get());
 			}
 		}
 		else
-			loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get());
+			loadSubObject(subData.second.getModScope(), subData.first, subData.second, newObject.get());
 	}
 
-	if (obj->id == MapObjectID::MONOLITH_TWO_WAY)
-		generateExtraMonolithsForRMG(obj.get());
+	if (newObject->id == MapObjectID::MONOLITH_TWO_WAY)
+		generateExtraMonolithsForRMG(newObject.get());
 
-	return obj;
+	return newObject;
 }
 
 void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data)
 {
-	objects.push_back(loadFromJson(scope, data, name, objects.size()));
+	mapObjectTypes.push_back(loadFromJson(scope, data, name, mapObjectTypes.size()));
 
-	VLC->identifiersHandler->registerObject(scope, "object", name, objects.back()->id);
+	VLC->identifiersHandler->registerObject(scope, "object", name, mapObjectTypes.back()->id);
 }
 
 void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index)
 {
-	assert(objects.at(index) == nullptr); // ensure that this id was not loaded before
+	assert(mapObjectTypes.at(index) == nullptr); // ensure that this id was not loaded before
 
-	objects.at(index) = loadFromJson(scope, data, name, index);
-	VLC->identifiersHandler->registerObject(scope, "object", name, objects.at(index)->id);
+	mapObjectTypes.at(index) = loadFromJson(scope, data, name, index);
+	VLC->identifiersHandler->registerObject(scope, "object", name, mapObjectTypes.at(index)->id);
 }
 
 void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID)
 {
 	config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not NULL
-	assert(objects.at(ID.getNum()));
 
-	if ( subID.getNum() >= objects.at(ID.getNum())->objects.size())
-		objects.at(ID.getNum())->objects.resize(subID.getNum()+1);
+	assert(mapObjectTypes.at(ID.getNum()));
 
-	JsonUtils::inherit(config, objects.at(ID.getNum())->base);
-	loadSubObject(config.getModScope(), identifier, config, objects.at(ID.getNum()).get(), subID.getNum());
+	if (subID.getNum() >= mapObjectTypes.at(ID.getNum())->objectTypeHandlers.size())
+	{
+		mapObjectTypes.at(ID.getNum())->objectTypeHandlers.resize(subID.getNum() + 1);
+	}
+
+	JsonUtils::inherit(config, mapObjectTypes.at(ID.getNum())->base);
+	loadSubObject(config.getModScope(), identifier, config, mapObjectTypes.at(ID.getNum()).get(), subID.getNum());
 }
 
 void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID)
 {
-	assert(objects.at(ID.getNum()));
-	objects.at(ID.getNum())->objects.at(subID.getNum()) = nullptr;
+	assert(mapObjectTypes.at(ID.getNum()));
+	mapObjectTypes.at(ID.getNum())->objectTypeHandlers.at(subID.getNum()) = nullptr;
 }
 
 TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const
 {
 	try
 	{
-		if (objects.at(type.getNum()) == nullptr)
-			return objects.front()->objects.front();
+		if (mapObjectTypes.at(type.getNum()) == nullptr)
+			return mapObjectTypes.front()->objectTypeHandlers.front();
 
 		auto subID = subtype.getNum();
 		if (type == Obj::PRISON)
 			subID = 0;
-		auto result = objects.at(type.getNum())->objects.at(subID);
+		auto result = mapObjectTypes.at(type.getNum())->objectTypeHandlers.at(subID);
 
 		if (result != nullptr)
 			return result;
@@ -365,11 +368,11 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scop
 	std::optional<si32> id = VLC->identifiers()->getIdentifier(scope, "object", type);
 	if(id)
 	{
-		const auto & object = objects.at(id.value());
+		const auto & object = mapObjectTypes.at(id.value());
 		std::optional<si32> subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype);
 
 		if (subID)
-			return object->objects.at(subID.value());
+			return object->objectTypeHandlers.at(subID.value());
 	}
 
 	std::string errorString = "Failed to find object of type " + type + "::" + subtype;
@@ -386,7 +389,7 @@ std::set<MapObjectID> CObjectClassesHandler::knownObjects() const
 {
 	std::set<MapObjectID> ret;
 
-	for(auto & entry : objects)
+	for(auto & entry : mapObjectTypes)
 		if (entry)
 			ret.insert(entry->id);
 
@@ -397,13 +400,13 @@ std::set<MapObjectSubID> CObjectClassesHandler::knownSubObjects(MapObjectID prim
 {
 	std::set<MapObjectSubID> ret;
 
-	if (!objects.at(primaryID.getNum()))
+	if (!mapObjectTypes.at(primaryID.getNum()))
 	{
 		logGlobal->error("Failed to find object %d", primaryID);
 		return ret;
 	}
 
-	for(const auto & entry : objects.at(primaryID.getNum())->objects)
+	for(const auto & entry : mapObjectTypes.at(primaryID.getNum())->objectTypeHandlers)
 		if (entry)
 			ret.insert(entry->subtype);
 
@@ -436,12 +439,12 @@ void CObjectClassesHandler::beforeValidate(JsonNode & object)
 
 void CObjectClassesHandler::afterLoadFinalization()
 {
-	for(auto & entry : objects)
+	for(auto & entry : mapObjectTypes)
 	{
 		if (!entry)
 			continue;
 
-		for(const auto & obj : entry->objects)
+		for(const auto & obj : entry->objectTypeHandlers)
 		{
 			if (!obj)
 				continue;
@@ -456,7 +459,7 @@ void CObjectClassesHandler::afterLoadFinalization()
 void CObjectClassesHandler::generateExtraMonolithsForRMG(ObjectClass * container)
 {
 	//duplicate existing two-way portals to make reserve for RMG
-	auto& portalVec = container->objects;
+	auto& portalVec = container->objectTypeHandlers;
 	//FIXME: Monoliths  in this vector can be already not useful for every terrain
 	const size_t portalCount = portalVec.size();
 
@@ -500,10 +503,10 @@ std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubI
 	if (handler && handler->hasNameTextID())
 		return handler->getNameTranslated();
 
-	if (objects.at(type.getNum()))
-		return objects.at(type.getNum())->getNameTranslated();
+	if (mapObjectTypes.at(type.getNum()))
+		return mapObjectTypes.at(type.getNum())->getNameTranslated();
 
-	return objects.front()->getNameTranslated();
+	return mapObjectTypes.front()->getNameTranslated();
 }
 
 SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObjectSubID subtype) const
@@ -515,27 +518,27 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObject
 	if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL)
 		subtype = 0;
 
-	if(objects.at(type.getNum()))
+	if(mapObjectTypes.at(type.getNum()))
 		return getHandlerFor(type, subtype)->getSounds();
 	else
-		return objects.front()->objects.front()->getSounds();
+		return mapObjectTypes.front()->objectTypeHandlers.front()->getSounds();
 }
 
 std::string CObjectClassesHandler::getObjectHandlerName(MapObjectID type) const
 {
-	if (objects.at(type.getNum()))
-		return objects.at(type.getNum())->handlerName;
+	if (mapObjectTypes.at(type.getNum()))
+		return mapObjectTypes.at(type.getNum())->handlerName;
 	else
-		return objects.front()->handlerName;
+		return mapObjectTypes.front()->handlerName;
 }
 
 std::string CObjectClassesHandler::getJsonKey(MapObjectID type) const
 {
-	if (objects.at(type.getNum()) != nullptr)
-		return objects.at(type.getNum())->getJsonKey();
+	if (mapObjectTypes.at(type.getNum()) != nullptr)
+		return mapObjectTypes.at(type.getNum())->getJsonKey();
 
 	logGlobal->warn("Unknown object of type %d!", type);
-	return objects.front()->getJsonKey();
+	return mapObjectTypes.front()->getJsonKey();
 }
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 2
lib/mapObjectConstructors/CObjectClassesHandler.h

@@ -55,7 +55,7 @@ public:
 	std::string handlerName; // ID of handler that controls this object, should be determined using handlerConstructor map
 
 	JsonNode base;
-	std::vector<TObjectTypeHandler> objects;
+	std::vector<TObjectTypeHandler> objectTypeHandlers;
 
 	ObjectClass();
 	~ObjectClass();
@@ -69,7 +69,7 @@ public:
 class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase, boost::noncopyable
 {
 	/// list of object handlers, each of them handles only one type
-	std::vector< std::unique_ptr<ObjectClass> > objects;
+	std::vector< std::unique_ptr<ObjectClass> > mapObjectTypes;
 
 	/// map that is filled during construction with all known handlers. Not serializeable due to usage of std::function
 	std::map<std::string, std::function<TObjectTypeHandler()> > handlerConstructors;

+ 0 - 5
lib/mapping/CMap.cpp

@@ -332,11 +332,6 @@ bool CMap::isCoastalTile(const int3 & pos) const
 	return false;
 }
 
-bool CMap::isInTheMap(const int3 & pos) const
-{
-	return pos.x >= 0 && pos.y >= 0 && pos.z >= 0 && pos.x < width && pos.y < height && pos.z <= (twoLevel ? 1 : 0);
-}
-
 TerrainTile & CMap::getTile(const int3 & tile)
 {
 	assert(isInTheMap(tile));

+ 8 - 1
lib/mapping/CMap.h

@@ -84,8 +84,15 @@ public:
 	TerrainTile & getTile(const int3 & tile);
 	const TerrainTile & getTile(const int3 & tile) const;
 	bool isCoastalTile(const int3 & pos) const;
-	bool isInTheMap(const int3 & pos) const;
 	bool isWaterTile(const int3 & pos) const;
+	inline bool isInTheMap(const int3 & pos) const
+	{
+		// Check whether coord < 0 is done implicitly. Negative signed int overflows to unsigned number larger than all signed ints.
+		return
+			static_cast<uint32_t>(pos.x) < static_cast<uint32_t>(width) &&
+			static_cast<uint32_t>(pos.y) < static_cast<uint32_t>(height) &&
+			static_cast<uint32_t>(pos.z) <= (twoLevel ? 1 : 0);
+	}
 
 	bool canMoveBetween(const int3 &src, const int3 &dst) const;
 	bool checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const;

+ 1 - 1
lib/spells/CSpellHandler.cpp

@@ -687,7 +687,7 @@ std::vector<int> CSpellHandler::spellRangeInHexes(std::string input) const
 	std::set<BattleHex> ret;
 	std::string rng = input + ','; //copy + artificial comma for easier handling
 
-	if(rng.size() >= 2 && rng[0] != 'X' && rng[0] != 'x') //there is at least one hex in range (+artificial comma)
+	if(rng.size() >= 2 && std::tolower(rng[0]) != 'x') //there is at least one hex in range (+artificial comma)
 	{
 		std::string number1;
 		std::string number2;

+ 3 - 3
mapeditor/graphics.cpp

@@ -48,9 +48,9 @@ void Graphics::loadPaletteAndColors()
 	for(int i = 0; i < 256; ++i)
 	{
 		QColor col;
-		col.setRed(pals[startPoint++]);
-		col.setGreen(pals[startPoint++]);
-		col.setBlue(pals[startPoint++]);
+		col.setRed(std::clamp(static_cast<int>(pals[startPoint++]), 0, 255));
+		col.setGreen(std::clamp(static_cast<int>(pals[startPoint++]), 0, 255));
+		col.setBlue(std::clamp(static_cast<int>(pals[startPoint++]), 0, 255));
 		col.setAlpha(255);
 		startPoint++;
 		playerColorPalette[i] = col.rgba();

+ 10 - 2
mapeditor/mapcontroller.cpp

@@ -381,9 +381,15 @@ void MapController::pasteFromClipboard(int level)
 	if(_clipboardShiftIndex == int3::getDirs().size())
 		_clipboardShiftIndex = 0;
 	
+	QStringList errors;
 	for(auto & objUniquePtr : _clipboard)
 	{
 		auto * obj = CMemorySerializer::deepCopy(*objUniquePtr).release();
+		QString errorMsg;
+		if (!canPlaceObject(level, obj, errorMsg))
+		{
+			errors.push_back(std::move(errorMsg));
+		}
 		auto newPos = objUniquePtr->pos + shift;
 		if(_map->isInTheMap(newPos))
 			obj->pos = newPos;
@@ -394,6 +400,8 @@ void MapController::pasteFromClipboard(int level)
 		_scenes[level]->selectionObjectsView.selectObject(obj);
 		_mapHandler->invalidate(obj);
 	}
+
+	QMessageBox::warning(main, QObject::tr("Can't place object"), errors.join('\n'));
 	
 	_scenes[level]->objectsView.draw();
 	_scenes[level]->passabilityView.update();
@@ -565,13 +573,13 @@ bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString
 	{
 		auto typeName = QString::fromStdString(newObj->typeName);
 		auto subTypeName = QString::fromStdString(newObj->subTypeName);
-		error = QString("There can be only one grail object on the map");
+		error = QObject::tr("There can only be one grail object on the map.");
 		return false; //maplimit reached
 	}
 	
 	if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO))
 	{
-		error = "Hero cannot be created as NEUTRAL";
+		error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(newObj->instanceName));
 		return false;
 	}
 	

+ 37 - 37
mapeditor/translation/chinese.ts

@@ -1438,37 +1438,37 @@
     <message>
         <location filename="../inspector/townbuildingswidget.ui" line="53"/>
         <source>Build all</source>
-        <translation type="unfinished"></translation>
+        <translation>建造所有建筑</translation>
     </message>
     <message>
         <location filename="../inspector/townbuildingswidget.ui" line="60"/>
         <source>Demolish all</source>
-        <translation type="unfinished"></translation>
+        <translation>拆除所有建筑</translation>
     </message>
     <message>
         <location filename="../inspector/townbuildingswidget.ui" line="67"/>
         <source>Enable all</source>
-        <translation type="unfinished"></translation>
+        <translation>允许所有建筑</translation>
     </message>
     <message>
         <location filename="../inspector/townbuildingswidget.ui" line="74"/>
         <source>Disable all</source>
-        <translation type="unfinished"></translation>
+        <translation>禁止所有建筑</translation>
     </message>
     <message>
         <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
         <source>Type</source>
-        <translation type="unfinished">类型</translation>
+        <translation>类型</translation>
     </message>
     <message>
         <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
         <source>Enabled</source>
-        <translation type="unfinished"></translation>
+        <translation>已允许</translation>
     </message>
     <message>
         <location filename="../inspector/townbuildingswidget.cpp" line="77"/>
         <source>Built</source>
-        <translation type="unfinished"></translation>
+        <translation>已建造</translation>
     </message>
 </context>
 <context>
@@ -1476,77 +1476,77 @@
     <message>
         <location filename="../inspector/towneventdialog.ui" line="23"/>
         <source>Town event</source>
-        <translation type="unfinished"></translation>
+        <translation>城镇事件</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="42"/>
         <source>General</source>
-        <translation type="unfinished">通用</translation>
+        <translation>通用</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="57"/>
         <source>Event name</source>
-        <translation type="unfinished">事件名</translation>
+        <translation>事件名</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="64"/>
         <source>Type event message text</source>
-        <translation type="unfinished">输入事件信息文本</translation>
+        <translation>输入事件信息文本</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="85"/>
         <source>Day of first occurrence</source>
-        <translation type="unfinished">首次发生天数</translation>
+        <translation>首次发生天数</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="99"/>
         <source>Repeat after (0 = no repeat)</source>
-        <translation type="unfinished">重复周期 (0 = 不重复)</translation>
+        <translation>重复周期 (0 = 不重复)</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="123"/>
         <source>Affected players</source>
-        <translation type="unfinished">生效玩家</translation>
+        <translation>生效玩家</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="146"/>
         <source>affects human</source>
-        <translation type="unfinished">人类玩家生效</translation>
+        <translation>人类玩家生效</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="155"/>
         <source>affects AI</source>
-        <translation type="unfinished">AI玩家生效</translation>
+        <translation>AI玩家生效</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="166"/>
         <source>Resources</source>
-        <translation type="unfinished">资源</translation>
+        <translation>资源</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="198"/>
         <source>Buildings</source>
-        <translation type="unfinished">建筑</translation>
+        <translation>建筑</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="216"/>
         <source>Creatures</source>
-        <translation type="unfinished">生物</translation>
+        <translation>生物</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.ui" line="255"/>
         <source>OK</source>
-        <translation type="unfinished"></translation>
+        <translation>确定</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.cpp" line="177"/>
         <source>Creature level %1 / Creature level %1 Upgrade</source>
-        <translation type="unfinished"></translation>
+        <translation>%1级生物 / 升级后的%1级生物</translation>
     </message>
     <message>
         <location filename="../inspector/towneventdialog.cpp" line="219"/>
         <source>Day %1 - %2</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 - %2 日</translation>
     </message>
 </context>
 <context>
@@ -1554,32 +1554,32 @@
     <message>
         <location filename="../inspector/towneventswidget.ui" line="29"/>
         <source>Town events</source>
-        <translation type="unfinished"></translation>
+        <translation>城镇事件</translation>
     </message>
     <message>
         <location filename="../inspector/towneventswidget.ui" line="37"/>
         <source>Timed events</source>
-        <translation type="unfinished">计时事件</translation>
+        <translation>计时事件</translation>
     </message>
     <message>
         <location filename="../inspector/towneventswidget.ui" line="63"/>
         <source>Add</source>
-        <translation type="unfinished">添加</translation>
+        <translation>添加</translation>
     </message>
     <message>
         <location filename="../inspector/towneventswidget.ui" line="76"/>
         <source>Remove</source>
-        <translation type="unfinished">移除</translation>
+        <translation>移除</translation>
     </message>
     <message>
         <location filename="../inspector/towneventswidget.cpp" line="105"/>
         <source>Day %1 - %2</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 - %2 日</translation>
     </message>
     <message>
         <location filename="../inspector/towneventswidget.cpp" line="126"/>
         <source>New event</source>
-        <translation type="unfinished">新事件</translation>
+        <translation>新事件</translation>
     </message>
 </context>
 <context>
@@ -1587,17 +1587,17 @@
     <message>
         <location filename="../inspector/townspellswidget.ui" line="29"/>
         <source>Spells</source>
-        <translation type="unfinished">魔法</translation>
+        <translation>魔法</translation>
     </message>
     <message>
         <location filename="../inspector/townspellswidget.ui" line="47"/>
         <source>Customize spells</source>
-        <translation type="unfinished">自定义魔法</translation>
+        <translation>自定义魔法</translation>
     </message>
     <message>
         <location filename="../inspector/townspellswidget.ui" line="76"/>
         <source>Level 1</source>
-        <translation type="unfinished">1级</translation>
+        <translation>1级</translation>
     </message>
     <message>
         <location filename="../inspector/townspellswidget.ui" line="93"/>
@@ -1606,7 +1606,7 @@
         <location filename="../inspector/townspellswidget.ui" line="231"/>
         <location filename="../inspector/townspellswidget.ui" line="277"/>
         <source>Spell that may appear in mage guild</source>
-        <translation type="unfinished"></translation>
+        <translation>允许出现在魔法行会的魔法</translation>
     </message>
     <message>
         <location filename="../inspector/townspellswidget.ui" line="100"/>
@@ -1615,27 +1615,27 @@
         <location filename="../inspector/townspellswidget.ui" line="238"/>
         <location filename="../inspector/townspellswidget.ui" line="284"/>
         <source>Spell that must appear in mage guild</source>
-        <translation type="unfinished"></translation>
+        <translation>必须出现在魔法行会的魔法</translation>
     </message>
     <message>
         <location filename="../inspector/townspellswidget.ui" line="122"/>
         <source>Level 2</source>
-        <translation type="unfinished">2级</translation>
+        <translation>2级</translation>
     </message>
     <message>
         <location filename="../inspector/townspellswidget.ui" line="168"/>
         <source>Level 3</source>
-        <translation type="unfinished">3级</translation>
+        <translation>3级</translation>
     </message>
     <message>
         <location filename="../inspector/townspellswidget.ui" line="214"/>
         <source>Level 4</source>
-        <translation type="unfinished">4级</translation>
+        <translation>4级</translation>
     </message>
     <message>
         <location filename="../inspector/townspellswidget.ui" line="260"/>
         <source>Level 5</source>
-        <translation type="unfinished">5级</translation>
+        <translation>5级</translation>
     </message>
 </context>
 <context>

+ 56 - 48
mapeditor/validator.cpp

@@ -43,13 +43,13 @@ Validator::~Validator()
 	delete ui;
 }
 
-std::list<Validator::Issue> Validator::validate(const CMap * map)
+std::set<Validator::Issue> Validator::validate(const CMap * map)
 {
-	std::list<Validator::Issue> issues;
+	std::set<Validator::Issue> issues;
 	
 	if(!map)
 	{
-		issues.emplace_back(tr("Map is not loaded"), true);
+		issues.insert({ tr("Map is not loaded"), true });
 		return issues;
 	}
 	
@@ -58,27 +58,29 @@ std::list<Validator::Issue> Validator::validate(const CMap * map)
 		//check player settings
 		int hplayers = 0;
 		int cplayers = 0;
-		std::map<int, int> amountOfCastles;
+		std::map<PlayerColor, int> amountOfTowns;
+		std::map<PlayerColor, int> amountOfHeroes;
+
 		for(int i = 0; i < map->players.size(); ++i)
 		{
 			auto & p = map->players[i];
-			if(p.canAnyonePlay())
-				amountOfCastles[i] = 0;
+			if (p.canAnyonePlay())
+				amountOfTowns[PlayerColor(i)] = 0;
 			if(p.canComputerPlay)
 				++cplayers;
 			if(p.canHumanPlay)
 				++hplayers;
 			if(p.allowedFactions.empty())
-				issues.emplace_back(QString(tr("No factions allowed for player %1")).arg(i), true);
+				issues.insert({ tr("No factions allowed for player %1").arg(i), true });
 		}
 		if(hplayers + cplayers == 0)
-			issues.emplace_back(tr("No players allowed to play this map"), true);
+			issues.insert({ tr("No players allowed to play this map"), true });
 		if(hplayers + cplayers == 1)
-			issues.emplace_back(tr("Map is allowed for one player and cannot be started"), true);
+			issues.insert({ tr("Map is allowed for one player and cannot be started"), true });
 		if(!hplayers)
-			issues.emplace_back(tr("No human players allowed to play this map"), true);
+			issues.insert({ tr("No human players allowed to play this map"), true });
 
-		std::set<const CHero*> allHeroesOnMap; //used to find hero duplicated
+		std::set<const CHero * > allHeroesOnMap; //used to find hero duplicated
 		
 		//checking all objects in the map
 		for(auto o : map->objects)
@@ -86,105 +88,111 @@ std::list<Validator::Issue> Validator::validate(const CMap * map)
 			//owners for objects
 			if(o->getOwner() == PlayerColor::UNFLAGGABLE)
 			{
-				if(dynamic_cast<CGMine*>(o.get()) ||
-				   dynamic_cast<CGDwelling*>(o.get()) ||
-				   dynamic_cast<CGTownInstance*>(o.get()) ||
-				   dynamic_cast<CGGarrison*>(o.get()) ||
-				   dynamic_cast<CGHeroInstance*>(o.get()))
+				if(dynamic_cast<CGMine *>(o.get()) ||
+				   dynamic_cast<CGDwelling *>(o.get()) ||
+				   dynamic_cast<CGTownInstance *>(o.get()) ||
+				   dynamic_cast<CGGarrison *>(o.get()) ||
+				   dynamic_cast<CGHeroInstance *>(o.get()))
 				{
-					issues.emplace_back(QString(tr("Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner")).arg(o->instanceName.c_str()), true);
+					issues.insert({ tr("Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner").arg(o->instanceName.c_str()), true });
 				}
 			}
 			if(o->getOwner() != PlayerColor::NEUTRAL && o->getOwner().getNum() < map->players.size())
 			{
 				if(!map->players[o->getOwner().getNum()].canAnyonePlay())
-					issues.emplace_back(QString(tr("Object %1 is assigned to non-playable player %2")).arg(o->instanceName.c_str(), o->getOwner().toString().c_str()), true);
+					issues.insert({ tr("Object %1 is assigned to non-playable player %2").arg(o->instanceName.c_str(), o->getOwner().toString().c_str()), true });
 			}
-			//checking towns
-			if(auto * ins = dynamic_cast<CGTownInstance*>(o.get()))
+			//count towns
+			if(auto * ins = dynamic_cast<CGTownInstance *>(o.get()))
 			{
-				bool has = amountOfCastles.count(ins->getOwner().getNum());
-				if(!has && ins->getOwner() != PlayerColor::NEUTRAL)
-					issues.emplace_back(tr("Town %1 has undefined owner %2").arg(ins->instanceName.c_str(), ins->getOwner().toString().c_str()), true);
-				if(has)
-					++amountOfCastles[ins->getOwner().getNum()];
+					++amountOfTowns[ins->getOwner()];
 			}
-			//checking heroes and prisons
-			if(auto * ins = dynamic_cast<CGHeroInstance*>(o.get()))
+			//checking and counting heroes and prisons
+			if(auto * ins = dynamic_cast<CGHeroInstance *>(o.get()))
 			{
 				if(ins->ID == Obj::PRISON)
 				{
 					if(ins->getOwner() != PlayerColor::NEUTRAL)
-						issues.emplace_back(QString(tr("Prison %1 must be a NEUTRAL")).arg(ins->instanceName.c_str()), true);
+						issues.insert({ tr("Prison %1 must be a NEUTRAL").arg(ins->instanceName.c_str()), true });
 				}
 				else
 				{
-					bool has = amountOfCastles.count(ins->getOwner().getNum());
-					if(!has)
-						issues.emplace_back(QString(tr("Hero %1 must have an owner")).arg(ins->instanceName.c_str()), true);
+					if(ins->getOwner() == PlayerColor::NEUTRAL)
+						issues.insert({ tr("Hero %1 must have an owner").arg(ins->instanceName.c_str()), true });
+
+					++amountOfHeroes[ins->getOwner()];
 				}
 				if(ins->type)
 				{
 					if(map->allowedHeroes.count(ins->getHeroType()) == 0)
-						issues.emplace_back(QString(tr("Hero %1 is prohibited by map settings")).arg(ins->type->getNameTranslated().c_str()), false);
+						issues.insert({ tr("Hero %1 is prohibited by map settings").arg(ins->type->getNameTranslated().c_str()), false });
 					
 					if(!allHeroesOnMap.insert(ins->type).second)
-						issues.emplace_back(QString(tr("Hero %1 has duplicate on map")).arg(ins->type->getNameTranslated().c_str()), false);
+						issues.insert({ tr("Hero %1 has duplicate on map").arg(ins->type->getNameTranslated().c_str()), false });
 				}
 				else if(ins->ID != Obj::RANDOM_HERO)
-					issues.emplace_back(QString(tr("Hero %1 has an empty type and must be removed")).arg(ins->instanceName.c_str()), true);
+					issues.insert({ tr("Hero %1 has an empty type and must be removed").arg(ins->instanceName.c_str()), true });
 			}
 			
 			//checking for arts
-			if(auto * ins = dynamic_cast<CGArtifact*>(o.get()))
+			if(auto * ins = dynamic_cast<CGArtifact *>(o.get()))
 			{
 				if(ins->ID == Obj::SPELL_SCROLL)
 				{
-					if(ins->storedArtifact)
+					if (ins->storedArtifact)
 					{
-						if(map->allowedSpells.count(ins->storedArtifact->getScrollSpellID()) == 0)
-							issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->storedArtifact->getScrollSpellID().toEntity(VLC->spells())->getNameTranslated().c_str()), false);
+						if (map->allowedSpells.count(ins->storedArtifact->getScrollSpellID()) == 0)
+							issues.insert({ tr("Spell scroll %1 is prohibited by map settings").arg(ins->storedArtifact->getScrollSpellID().toEntity(VLC->spells())->getNameTranslated().c_str()), false });
 					}
 					else
-						issues.emplace_back(QString(tr("Spell scroll %1 doesn't have instance assigned and must be removed")).arg(ins->instanceName.c_str()), true);
+						issues.insert({ tr("Spell scroll % 1 doesn't have instance assigned and must be removed").arg(ins->instanceName.c_str()), true });
 				}
 				else
 				{
 					if(ins->ID == Obj::ARTIFACT && map->allowedArtifact.count(ins->getArtifact()) == 0)
 					{
-						issues.emplace_back(QString(tr("Artifact %1 is prohibited by map settings")).arg(ins->getObjectName().c_str()), false);
+						issues.insert({ tr("Artifact % 1 is prohibited by map settings").arg(ins->getObjectName().c_str()), false });
 					}
 				}
 			}
 		}
 
 		//verification of starting towns
-		for(auto & mp : amountOfCastles)
-			if(mp.second == 0)
-				issues.emplace_back(QString(tr("Player %1 doesn't have any starting town")).arg(mp.first), false);
+		for (const auto & [player, counter] : amountOfTowns)
+		{
+			if (counter == 0)
+			{
+				// FIXME: heroesNames are empty even though heroes are on the map
+				// if(map->players[playerTCounter.first].heroesNames.empty())
+				if(amountOfHeroes.count(player) == 0)
+					issues.insert({ tr("Player %1 has no towns and heroes assigned").arg(player + 1), true });
+				else
+					issues.insert({ tr("Player %1 doesn't have any starting town").arg(player + 1), false });
+			}
+		}
 
 		//verification of map name and description
 		if(map->name.empty())
-			issues.emplace_back(tr("Map name is not specified"), false);
+			issues.insert({ tr("Map name is not specified"), false });
 		if(map->description.empty())
-			issues.emplace_back(tr("Map description is not specified"), false);
+			issues.insert({ tr("Map description is not specified"), false });
 		
 		//verificationfor mods
 		for(auto & mod : MapController::modAssessmentMap(*map))
 		{
 			if(!map->mods.count(mod.first))
 			{
-				issues.emplace_back(QString(tr("Map contains object from mod \"%1\", but doesn't require it")).arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).getVerificationInfo().name)), true);
+				issues.insert({ tr("Map contains object from mod \"%1\", but doesn't require it").arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).getVerificationInfo().name)), true });
 			}
 		}
 	}
 	catch(const std::exception & e)
 	{
-		issues.emplace_back(QString(tr("Exception occurs during validation: %1")).arg(e.what()), true);
+		issues.insert({ tr("Exception occurs during validation: %1").arg(e.what()), true });
 	}
 	catch(...)
 	{
-		issues.emplace_back(tr("Unknown exception occurs during validation"), true);
+		issues.insert({ tr("Unknown exception occurs during validation"), true });
 	}
 	
 	return issues;

+ 7 - 1
mapeditor/validator.h

@@ -11,6 +11,7 @@
 #pragma once
 
 #include <QDialog>
+#include <set>
 
 VCMI_LIB_NAMESPACE_BEGIN
 class CMap;
@@ -30,13 +31,18 @@ public:
 		bool critical;
 		
 		Issue(const QString & m, bool c): message(m), critical(c) {}
+
+		bool operator <(const Issue & other) const
+		{
+			return message < other.message;
+		}
 	};
 	
 public:
 	explicit Validator(const CMap * map, QWidget *parent = nullptr);
 	~Validator();
 	
-	static std::list<Issue> validate(const CMap * map);
+	static std::set<Issue> validate(const CMap * map);
 
 private:
 	Ui::Validator *ui;