Просмотр исходного кода

Merge pull request #3432 from IvanSavenko/crashfixes

[1.4.3] Crashfixes
Ivan Savenko 1 год назад
Родитель
Сommit
1834db4177

+ 1 - 1
client/CMT.cpp

@@ -205,7 +205,7 @@ int main(int argc, char * argv[])
 	logGlobal->info("The log file will be saved to %s", logPath);
 
 	// Init filesystem and settings
-	preinitDLL(::console);
+	preinitDLL(::console, false);
 
 	Settings session = settings.write["session"];
 	auto setSettingBool = [](std::string key, std::string arg) {

+ 1 - 0
client/Client.h

@@ -164,6 +164,7 @@ public:
 	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;};
 	void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override {};
 	void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {};
+	void giveExperience(const CGHeroInstance * hero, TExpType val) override {};
 	void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {};
 	void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {};
 

+ 4 - 2
client/adventureMap/MapAudioPlayer.cpp

@@ -173,8 +173,10 @@ void MapAudioPlayer::updateMusic()
 {
 	if(audioPlaying && playerMakingTurn && currentSelection)
 	{
-		const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType;
-		CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false);
+		const auto * tile = LOCPLINT->cb->getTile(currentSelection->visitablePos());
+
+		if (tile)
+			CCS->musich->playMusicFromSet("terrain", tile->terType->getJsonKey(), true, false);
 	}
 
 	if(audioPlaying && enemyMakingTurn)

+ 1 - 1
client/lobby/CSelectionBase.cpp

@@ -69,7 +69,7 @@ int ISelectionScreenInfo::getCurrentDifficulty()
 
 PlayerInfo ISelectionScreenInfo::getPlayerInfo(PlayerColor color)
 {
-	return getMapInfo()->mapHeader->players[color.getNum()];
+	return getMapInfo()->mapHeader->players.at(color.getNum());
 }
 
 CSelectionBase::CSelectionBase(ESelectionScreen type)

+ 2 - 2
lib/CBonusTypeHandler.cpp

@@ -76,10 +76,10 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr<Bonus> & bonu
 	if (text.find("${val}") != std::string::npos)
 		boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype))));
 
-	if (text.find("${subtype.creature}") != std::string::npos)
+	if (text.find("${subtype.creature}") != std::string::npos && bonus->subtype.as<CreatureID>() != CreatureID::NONE)
 		boost::algorithm::replace_all(text, "${subtype.creature}", bonus->subtype.as<CreatureID>().toCreature()->getNamePluralTranslated());
 
-	if (text.find("${subtype.spell}") != std::string::npos)
+	if (text.find("${subtype.spell}") != std::string::npos && bonus->subtype.as<SpellID>() != SpellID::NONE)
 		boost::algorithm::replace_all(text, "${subtype.spell}", bonus->subtype.as<SpellID>().toSpell()->getNameTranslated());
 
 	return text;

+ 19 - 7
lib/CHeroHandler.cpp

@@ -654,14 +654,21 @@ void CHeroHandler::loadExperience()
 	expPerLevel.push_back(24320);
 	expPerLevel.push_back(28784);
 	expPerLevel.push_back(34140);
-	while (expPerLevel[expPerLevel.size() - 1] > expPerLevel[expPerLevel.size() - 2])
+
+	for (;;)
 	{
 		auto i = expPerLevel.size() - 1;
-		auto diff = expPerLevel[i] - expPerLevel[i-1];
-		diff += diff / 5;
-		expPerLevel.push_back (expPerLevel[i] + diff);
+		auto currExp = expPerLevel[i];
+		auto prevExp = expPerLevel[i-1];
+		auto prevDiff = currExp - prevExp;
+		auto nextDiff = prevDiff + prevDiff / 5;
+		auto maxExp = std::numeric_limits<decltype(currExp)>::max();
+
+		if (currExp > maxExp - nextDiff)
+			break; // overflow point reached
+
+		expPerLevel.push_back (currExp + nextDiff);
 	}
-	expPerLevel.pop_back();//last value is broken
 }
 
 /// convert h3-style ID (e.g. Gobin Wolf Rider) to vcmi (e.g. goblinWolfRider)
@@ -741,12 +748,12 @@ void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNod
 	registerObject(scope, "hero", name, object->getIndex());
 }
 
-ui32 CHeroHandler::level (ui64 experience) const
+ui32 CHeroHandler::level (TExpType experience) const
 {
 	return static_cast<ui32>(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel));
 }
 
-ui64 CHeroHandler::reqExp (ui32 level) const
+TExpType CHeroHandler::reqExp (ui32 level) const
 {
 	if(!level)
 		return 0;
@@ -762,6 +769,11 @@ ui64 CHeroHandler::reqExp (ui32 level) const
 	}
 }
 
+ui32 CHeroHandler::maxSupportedLevel() const
+{
+	return expPerLevel.size();
+}
+
 std::set<HeroTypeID> CHeroHandler::getDefaultAllowed() const
 {
 	std::set<HeroTypeID> result;

+ 5 - 4
lib/CHeroHandler.h

@@ -176,8 +176,8 @@ protected:
 class DLL_LINKAGE CHeroHandler : public CHandlerBase<HeroTypeID, HeroType, CHero, HeroTypeService>
 {
 	/// expPerLEvel[i] is amount of exp needed to reach level i;
-	/// consists of 201 values. Any higher levels require experience larger that ui64 can hold
-	std::vector<ui64> expPerLevel;
+	/// consists of 196 values. Any higher levels require experience larger that TExpType can hold
+	std::vector<TExpType> expPerLevel;
 
 	/// helpers for loading to avoid huge load functions
 	void loadHeroArmy(CHero * hero, const JsonNode & node) const;
@@ -191,8 +191,9 @@ class DLL_LINKAGE CHeroHandler : public CHandlerBase<HeroTypeID, HeroType, CHero
 public:
 	CHeroClassHandler classes;
 
-	ui32 level(ui64 experience) const; //calculates level corresponding to given experience amount
-	ui64 reqExp(ui32 level) const; //calculates experience required for given level
+	ui32 level(TExpType experience) const; //calculates level corresponding to given experience amount
+	TExpType reqExp(ui32 level) const; //calculates experience required for given level
+	ui32 maxSupportedLevel() const;
 
 	std::vector<JsonNode> loadLegacyData() override;
 

+ 1 - 0
lib/IGameCallback.h

@@ -84,6 +84,7 @@ public:
 	virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0;
 	virtual void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) = 0;
 	virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0;
+	virtual void giveExperience(const CGHeroInstance * hero, TExpType val) =0;
 	virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0;
 	virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0;
 	virtual void showBlockingDialog(BlockingDialog *iw) =0;

+ 4 - 4
lib/VCMI_Lib.cpp

@@ -47,14 +47,14 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 LibClasses * VLC = nullptr;
 
-DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool extractArchives)
+DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool extractArchives)
 {
 	console = Console;
 	VLC = new LibClasses();
 	VLC->loadFilesystem(extractArchives);
 	settings.init("config/settings.json", "vcmi:settings");
 	persistentStorage.init("config/persistentStorage.json", "");
-	VLC->loadModFilesystem(onlyEssential);
+	VLC->loadModFilesystem();
 
 }
 
@@ -182,12 +182,12 @@ void LibClasses::loadFilesystem(bool extractArchives)
 	logGlobal->info("\tData loading: %d ms", loadTime.getDiff());
 }
 
-void LibClasses::loadModFilesystem(bool onlyEssential)
+void LibClasses::loadModFilesystem()
 {
 	CStopWatch loadTime;
 	modh = new CModHandler();
 	identifiersHandler = new CIdentifierStorage();
-	modh->loadMods(onlyEssential);
+	modh->loadMods();
 	logGlobal->info("\tMod handler: %d ms", loadTime.getDiff());
 
 	modh->loadModFilesystems();

+ 2 - 2
lib/VCMI_Lib.h

@@ -115,7 +115,7 @@ public:
 
 	// basic initialization. should be called before init(). Can also extract original H3 archives
 	void loadFilesystem(bool extractArchives);
-	void loadModFilesystem(bool onlyEssential);
+	void loadModFilesystem();
 
 #if SCRIPTING_ENABLED
 	void scriptsLoaded();
@@ -124,7 +124,7 @@ public:
 
 extern DLL_LINKAGE LibClasses * VLC;
 
-DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false, bool extractArchives = false);
+DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool extractArchives);
 DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false);
 
 

+ 2 - 7
lib/constants/EntityIdentifiers.cpp

@@ -292,7 +292,7 @@ const Skill * SecondarySkill::toEntity(const Services * services) const
 
 const CCreature * CreatureIDBase::toCreature() const
 {
-	return VLC->creh->objects.at(num);
+	return dynamic_cast<const CCreature *>(toEntity(VLC));
 }
 
 const Creature * CreatureIDBase::toEntity(const Services * services) const
@@ -324,12 +324,7 @@ std::string CreatureID::entityType()
 
 const CSpell * SpellIDBase::toSpell() const
 {
-	if(num < 0 || num >= VLC->spellh->objects.size())
-	{
-		logGlobal->error("Unable to get spell of invalid ID %d", static_cast<int>(num));
-		return nullptr;
-	}
-	return VLC->spellh->objects[num];
+	return dynamic_cast<const CSpell*>(toEntity(VLC));
 }
 
 const spells::Spell * SpellIDBase::toEntity(const Services * services) const

+ 28 - 20
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -177,8 +177,10 @@ void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::
 	auto object = loadSubObjectFromJson(scope, identifier, entry, obj, index);
 
 	assert(object);
-	assert(obj->objects[index] == nullptr); // ensure that this id was not loaded before
-	obj->objects[index] = object;
+	if (obj->objects.at(index) != nullptr)
+		throw std::runtime_error("Attempt to load already loaded object:" + identifier);
+
+	obj->objects.at(index) = object;
 
 	registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype);
 	for(const auto & compatID : entry["compatibilityIdentifiers"].Vector())
@@ -259,10 +261,16 @@ std::unique_ptr<ObjectClass> CObjectClassesHandler::loadFromJson(const std::stri
 		{
 			const std::string & subMeta = subData.second["index"].meta;
 
-			if ( subMeta != "core")
-				logMod->warn("Object %s:%s.%s - attempt to load object with preset index! This option is reserved for built-in mod", subMeta, name, subData.first );
-			size_t subIndex = subData.second["index"].Integer();
-			loadSubObject(subData.second.meta, subData.first, subData.second, obj.get(), subIndex);
+			if ( subMeta == "core")
+			{
+				size_t subIndex = subData.second["index"].Integer();
+				loadSubObject(subData.second.meta, subData.first, subData.second, obj.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.meta, subData.first, subData.second, obj.get());
+			}
 		}
 		else
 			loadSubObject(subData.second.meta, subData.first, subData.second, obj.get());
@@ -283,28 +291,28 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons
 
 void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index)
 {
-	assert(objects[index] == nullptr); // ensure that this id was not loaded before
+	assert(objects.at(index) == nullptr); // ensure that this id was not loaded before
 
-	objects[index] = loadFromJson(scope, data, name, index);
-	VLC->identifiersHandler->registerObject(scope, "object", name, objects[index]->id);
+	objects.at(index) = loadFromJson(scope, data, name, index);
+	VLC->identifiersHandler->registerObject(scope, "object", name, objects.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[ID.getNum()]);
+	assert(objects.at(ID.getNum()));
 
-	if ( subID.getNum() >= objects[ID.getNum()]->objects.size())
-		objects[ID.getNum()]->objects.resize(subID.getNum()+1);
+	if ( subID.getNum() >= objects.at(ID.getNum())->objects.size())
+		objects.at(ID.getNum())->objects.resize(subID.getNum()+1);
 
 	JsonUtils::inherit(config, objects.at(ID.getNum())->base);
-	loadSubObject(config.meta, identifier, config, objects[ID.getNum()].get(), subID.getNum());
+	loadSubObject(config.meta, identifier, config, objects.at(ID.getNum()).get(), subID.getNum());
 }
 
 void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID)
 {
-	assert(objects[ID.getNum()]);
-	objects[ID.getNum()]->objects[subID.getNum()] = nullptr;
+	assert(objects.at(ID.getNum()));
+	objects.at(ID.getNum())->objects.at(subID.getNum()) = nullptr;
 }
 
 TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const
@@ -337,11 +345,11 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scop
 	std::optional<si32> id = VLC->identifiers()->getIdentifier(scope, "object", type);
 	if(id)
 	{
-		const auto & object = objects[id.value()];
+		const auto & object = objects.at(id.value());
 		std::optional<si32> subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype);
 
 		if (subID)
-			return object->objects[subID.value()];
+			return object->objects.at(subID.value());
 	}
 
 	std::string errorString = "Failed to find object of type " + type + "::" + subtype;
@@ -472,8 +480,8 @@ std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubI
 	if (handler && handler->hasNameTextID())
 		return handler->getNameTranslated();
 
-	if (objects[type.getNum()])
-		return objects[type.getNum()]->getNameTranslated();
+	if (objects.at(type.getNum()))
+		return objects.at(type.getNum())->getNameTranslated();
 
 	return objects.front()->getNameTranslated();
 }
@@ -487,7 +495,7 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObject
 	if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL)
 		subtype = 0;
 
-	if(objects[type.getNum()])
+	if(objects.at(type.getNum()))
 		return getHandlerFor(type, subtype)->getSounds();
 	else
 		return objects.front()->objects.front()->getSounds();

+ 1 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -1439,7 +1439,7 @@ void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8
 
 bool CGHeroInstance::gainsLevel() const
 {
-	return exp >= static_cast<TExpType>(VLC->heroh->reqExp(level+1));
+	return level < VLC->heroh->maxSupportedLevel() && exp >= static_cast<TExpType>(VLC->heroh->reqExp(level+1));
 }
 
 void CGHeroInstance::levelUp(const std::vector<SecondarySkill> & skills)

+ 6 - 2
lib/mapObjects/CGTownBuilding.cpp

@@ -249,8 +249,12 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const
 			iw.player = cb->getOwner(heroID);
 				iw.text.appendRawString(getVisitingBonusGreeting());
 			cb->showInfoDialog(&iw);
-			cb->changePrimSkill (cb->getHero(heroID), what, val);
-				town->addHeroToStructureVisitors(h, indexOnTV);
+			if (what == PrimarySkill::EXPERIENCE)
+				cb->giveExperience(cb->getHero(heroID), val);
+			else
+				cb->changePrimSkill(cb->getHero(heroID), what, val);
+
+			town->addHeroToStructureVisitors(h, indexOnTV);
 		}
 	}
 }

+ 1 - 1
lib/mapObjects/MiscObjects.cpp

@@ -1138,7 +1138,7 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const
 			xp = h->calculateXp(static_cast<int>(xp));
 			iw.text.appendLocalString(EMetaText::ADVOB_TXT,132);
 			iw.text.replaceNumber(static_cast<int>(xp));
-			cb->changePrimSkill(h, PrimarySkill::EXPERIENCE, xp, false);
+			cb->giveExperience(h, xp);
 		}
 		else
 		{

+ 19 - 21
lib/modding/CModHandler.cpp

@@ -237,19 +237,12 @@ void CModHandler::loadOneMod(std::string modName, const std::string & parent, co
 	}
 }
 
-void CModHandler::loadMods(bool onlyEssential)
+void CModHandler::loadMods()
 {
 	JsonNode modConfig;
 
-	if(onlyEssential)
-	{
-		loadOneMod("vcmi", "", modConfig, true);//only vcmi and submods
-	}
-	else
-	{
-		modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json"));
-		loadMods("", "", modConfig["activeMods"], true);
-	}
+	modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json"));
+	loadMods("", "", modConfig["activeMods"], true);
 
 	coreMod = std::make_unique<CModInfo>(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json")));
 }
@@ -346,20 +339,25 @@ void CModHandler::loadModFilesystems()
 
 TModID CModHandler::findResourceOrigin(const ResourcePath & name) const
 {
-	for(const auto & modID : boost::adaptors::reverse(activeMods))
+	try
 	{
-		if(CResourceHandler::get(modID)->existsResource(name))
-			return modID;
-	}
-
-	if(CResourceHandler::get("core")->existsResource(name))
-		return "core";
+		for(const auto & modID : boost::adaptors::reverse(activeMods))
+		{
+			if(CResourceHandler::get(modID)->existsResource(name))
+				return modID;
+		}
 
-	if(CResourceHandler::get("mapEditor")->existsResource(name))
-		return "core"; // Workaround for loading maps via map editor
+		if(CResourceHandler::get("core")->existsResource(name))
+			return "core";
 
-	assert(0);
-	return "";
+		if(CResourceHandler::get("mapEditor")->existsResource(name))
+			return "core"; // Workaround for loading maps via map editor
+	}
+	catch( const std::out_of_range & e)
+	{
+		// no-op
+	}
+	throw std::runtime_error("Resource with name " + name.getName() + " and type " + EResTypeHelper::getEResTypeAsString(name.getType()) + " wasn't found.");
 }
 
 std::string CModHandler::getModLanguage(const TModID& modId) const

+ 1 - 1
lib/modding/CModHandler.h

@@ -58,7 +58,7 @@ public:
 
 	/// receives list of available mods and trying to load mod.json from all of them
 	void initializeConfig();
-	void loadMods(bool onlyEssential = false);
+	void loadMods();
 	void loadModFilesystems();
 
 	/// returns ID of mod that provides selected file resource

+ 6 - 4
lib/modding/ContentTypeHandler.cpp

@@ -115,11 +115,13 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate)
 			continue;
 		}
 
-		if (vstd::contains(data.Struct(), "index") && !data["index"].isNull())
-		{
-			if (modName != "core")
-				logMod->warn("Mod %s is attempting to load original data! This should be reserved for built-in mod.", modName);
+		bool hasIndex = vstd::contains(data.Struct(), "index") && !data["index"].isNull();
+
+		if (hasIndex && modName != "core")
+			logMod->error("Mod %s is attempting to load original data! This option is reserved for built-in mod.", modName);
 
+		if (hasIndex && modName == "core")
+		{
 			// try to add H3 object data
 			size_t index = static_cast<size_t>(data["index"].Float());
 

+ 2 - 2
lib/rewardable/Interface.cpp

@@ -117,7 +117,7 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R
 	for(int i=0; i< info.reward.primary.size(); i++)
 		cb->changePrimSkill(hero, static_cast<PrimarySkill>(i), info.reward.primary[i], false);
 
-	si64 expToGive = 0;
+	TExpType expToGive = 0;
 
 	if (info.reward.heroLevel > 0)
 		expToGive += VLC->heroh->reqExp(hero->level+info.reward.heroLevel) - VLC->heroh->reqExp(hero->level);
@@ -126,7 +126,7 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R
 		expToGive += hero->calculateXp(info.reward.heroExperience);
 
 	if(expToGive)
-		cb->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expToGive);
+		cb->giveExperience(hero, expToGive);
 }
 
 void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Rewardable::VisitInfo & info, const CArmedInstance * army, const CGHeroInstance * hero) const

+ 1 - 1
mapeditor/mainwindow.cpp

@@ -177,7 +177,7 @@ MainWindow::MainWindow(QWidget* parent) :
 	logGlobal->info("The log file will be saved to %s", logPath);
 
 	//init
-	preinitDLL(::console, false, extractionOptions.extractArchives);
+	preinitDLL(::console, extractionOptions.extractArchives);
 
 	// Initialize logging based on settings
 	logConfig->configure();

+ 46 - 38
server/CGameHandler.cpp

@@ -365,52 +365,60 @@ void CGameHandler::expGiven(const CGHeroInstance *hero)
 // 		levelUpHero(hero);
 }
 
-void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs)
+void CGameHandler::giveExperience(const CGHeroInstance * hero, TExpType amountToGain)
 {
-	if (which == PrimarySkill::EXPERIENCE) // Check if scenario limit reached
+	TExpType maxExp = VLC->heroh->reqExp(VLC->heroh->maxSupportedLevel());
+	TExpType currExp = hero->exp;
+
+	if (gs->map->levelLimit != 0)
+		maxExp = VLC->heroh->reqExp(gs->map->levelLimit);
+
+	TExpType canGainExp = 0;
+	if (maxExp > currExp)
+		canGainExp = maxExp - currExp;
+
+	if (amountToGain > canGainExp)
 	{
-		if (gs->map->levelLimit != 0)
-		{
-			TExpType expLimit = VLC->heroh->reqExp(gs->map->levelLimit);
-			TExpType resultingExp = abs ? val : hero->exp + val;
-			if (resultingExp > expLimit)
-			{
-				// set given experience to max possible, but don't decrease if hero already over top
-				abs = true;
-				val = std::max(expLimit, hero->exp);
+		// set given experience to max possible, but don't decrease if hero already over top
+		amountToGain = canGainExp;
 
-				InfoWindow iw;
-				iw.player = hero->tempOwner;
-				iw.text.appendLocalString(EMetaText::GENERAL_TXT, 1); //can gain no more XP
-				iw.text.replaceRawString(hero->getNameTranslated());
-				sendAndApply(&iw);
-			}
-		}
+		InfoWindow iw;
+		iw.player = hero->tempOwner;
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 1); //can gain no more XP
+		iw.text.replaceRawString(hero->getNameTranslated());
+		sendAndApply(&iw);
 	}
 
 	SetPrimSkill sps;
 	sps.id = hero->id;
-	sps.which = which;
-	sps.abs = abs;
-	sps.val = val;
+	sps.which = PrimarySkill::EXPERIENCE;
+	sps.abs = false;
+	sps.val = amountToGain;
 	sendAndApply(&sps);
 
-	//only for exp - hero may level up
-	if (which == PrimarySkill::EXPERIENCE)
+	//hero may level up
+	if (hero->commander && hero->commander->alive)
 	{
-		if (hero->commander && hero->commander->alive)
-		{
-			//FIXME: trim experience according to map limit?
-			SetCommanderProperty scp;
-			scp.heroid = hero->id;
-			scp.which = SetCommanderProperty::EXPERIENCE;
-			scp.amount = val;
-			sendAndApply (&scp);
-			CBonusSystemNode::treeHasChanged();
-		}
-
-		expGiven(hero);
+		//FIXME: trim experience according to map limit?
+		SetCommanderProperty scp;
+		scp.heroid = hero->id;
+		scp.which = SetCommanderProperty::EXPERIENCE;
+		scp.amount = amountToGain;
+		sendAndApply (&scp);
+		CBonusSystemNode::treeHasChanged();
 	}
+
+	expGiven(hero);
+}
+
+void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs)
+{
+	SetPrimSkill sps;
+	sps.id = hero->id;
+	sps.which = which;
+	sps.abs = abs;
+	sps.val = val;
+	sendAndApply(&sps);
 }
 
 void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs)
@@ -658,7 +666,7 @@ void CGameHandler::onNewTurn()
 		{
 			if (obj && obj->ID == Obj::PRISON) //give imprisoned hero 0 exp to level him up. easiest to do at this point
 			{
-				changePrimSkill (getHero(obj->id), PrimarySkill::EXPERIENCE, 0);
+				giveExperience(getHero(obj->id), 0);
 			}
 		}
 	}
@@ -3708,7 +3716,7 @@ bool CGameHandler::sacrificeCreatures(const IMarket * market, const CGHeroInstan
 	int expSum = 0;
 	auto finish = [this, &hero, &expSum]()
 	{
-		changePrimSkill(hero, PrimarySkill::EXPERIENCE, hero->calculateXp(expSum));
+		giveExperience(hero, hero->calculateXp(expSum));
 	};
 
 	for(int i = 0; i < slot.size(); ++i)
@@ -3749,7 +3757,7 @@ bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * h
 	int expSum = 0;
 	auto finish = [this, &hero, &expSum]()
 	{
-		changePrimSkill(hero, PrimarySkill::EXPERIENCE, hero->calculateXp(expSum));
+		giveExperience(hero, hero->calculateXp(expSum));
 	};
 
 	for(int i = 0; i < slot.size(); ++i)

+ 1 - 0
server/CGameHandler.h

@@ -102,6 +102,7 @@ public:
 	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override;
 	void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override;
 	void setOwner(const CGObjectInstance * obj, PlayerColor owner) override;
+	void giveExperience(const CGHeroInstance * hero, TExpType val) override;
 	void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override;
 	void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override;
 

+ 6 - 2
server/CVCMIServer.cpp

@@ -141,7 +141,11 @@ CVCMIServer::CVCMIServer(boost::program_options::variables_map & opts)
 		if(cmdLineOptions.count("run-by-client"))
 		{
 			logNetwork->error("Port must be specified when run-by-client is used!!");
-			exit(0);
+#if (defined(__ANDROID_API__) && __ANDROID_API__ < 21) || (defined(__MINGW32__)) || defined(VCMI_APPLE)
+			::exit(0);
+#else
+			std::quick_exit(0);
+#endif
 		}
 		acceptor = std::make_shared<TAcceptor>(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0));
 		port = acceptor->local_endpoint().port();
@@ -1172,7 +1176,7 @@ int main(int argc, const char * argv[])
 
 	boost::program_options::variables_map opts;
 	handleCommandOptions(argc, argv, opts);
-	preinitDLL(console);
+	preinitDLL(console, false);
 	logConfig.configure();
 
 	loadDLLClasses();

+ 7 - 1
server/TurnTimerHandler.cpp

@@ -94,8 +94,14 @@ void TurnTimerHandler::update(int waitTime)
 			if(gs->isPlayerMakingTurn(player))
 				onPlayerMakingTurn(player, waitTime);
 		
+		// create copy for iterations - battle might end during onBattleLoop call
+		std::vector<BattleID> ongoingBattles;
+
 		for (auto & battle : gs->currentBattles)
-			onBattleLoop(battle->battleID, waitTime);
+			ongoingBattles.push_back(battle->battleID);
+
+		for (auto & battleID : ongoingBattles)
+			onBattleLoop(battleID, waitTime);
 	}
 }
 

+ 5 - 0
server/battles/BattleFlowProcessor.cpp

@@ -712,6 +712,11 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c
 			}
 		}
 		BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER)));
+		bl.remove_if([](const Bonus * b)
+		{
+			return b->subtype.as<SpellID>() == SpellID::NONE;
+		});
+
 		int side = *battle.playerToSide(st->unitOwner());
 		if(st->canCast() && battle.battleGetEnchanterCounter(side) == 0)
 		{

+ 1 - 1
server/battles/BattleResultProcessor.cpp

@@ -494,7 +494,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle)
 	}
 	//give exp
 	if(!finishingBattle->isDraw() && battleResult->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero)
-		gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]);
+		gameHandler->giveExperience(finishingBattle->winnerHero, battleResult->exp[finishingBattle->winnerSide]);
 
 	BattleResultAccepted raccepted;
 	raccepted.battleID = battle.getBattle()->getBattleID();

+ 2 - 2
server/processors/PlayerMessageProcessor.cpp

@@ -261,7 +261,7 @@ void PlayerMessageProcessor::cheatLevelup(PlayerColor player, const CGHeroInstan
 		levelsToGain = 1;
 	}
 
-	gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level + levelsToGain) - VLC->heroh->reqExp(hero->level));
+	gameHandler->giveExperience(hero, VLC->heroh->reqExp(hero->level + levelsToGain) - VLC->heroh->reqExp(hero->level));
 }
 
 void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words)
@@ -280,7 +280,7 @@ void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroIns
 		expAmountProcessed = 10000;
 	}
 
-	gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expAmountProcessed);
+	gameHandler->giveExperience(hero, expAmountProcessed);
 }
 
 void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words)

+ 1 - 0
test/mock/mock_IGameCallback.h

@@ -44,6 +44,7 @@ public:
 	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}
 	void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override {};
 	void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {}
+	void giveExperience(const CGHeroInstance * hero, TExpType val) override {}
 	void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override {}
 	void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override {}
 	void showBlockingDialog(BlockingDialog *iw) override {}