Browse Source

Merge pull request #3335 from vcmi/master

Merge master -> beta
Ivan Savenko 1 year ago
parent
commit
e6dcf355e8

+ 1 - 0
Mods/vcmi/config/vcmi/english.json

@@ -69,6 +69,7 @@
 	"vcmi.lobby.noPreview" : "no preview",
 	"vcmi.lobby.noUnderground" : "no underground",
 
+	"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",
 	"vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",
 	"vcmi.server.errors.modsToEnable"    : "{Following mods are required}",
 	"vcmi.server.errors.modsToDisable"   : "{Following mods must be disabled}",

+ 1 - 0
Mods/vcmi/config/vcmi/ukrainian.json

@@ -69,6 +69,7 @@
 	"vcmi.lobby.noPreview" : "огляд недоступний",
 	"vcmi.lobby.noUnderground" : "немає підземелля",
 
+	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",
 	"vcmi.server.errors.modsToDisable"   : "{Модифікації що мають бути вимкнені}",

+ 1 - 1
android/vcmi-app/build.gradle

@@ -10,7 +10,7 @@ android {
 		applicationId "is.xyz.vcmi"
 		minSdk 19
 		targetSdk 33
-		versionCode 1410
+		versionCode 1412
 		versionName "1.4.1"
 		setProperty("archivesBaseName", "vcmi")
 	}

+ 15 - 8
client/CMT.cpp

@@ -257,10 +257,10 @@ int main(int argc, char * argv[])
 	};
 
 	testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!");
-	testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!");
-	testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them.");
-	testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them.");
 	testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!");
+	testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!");
+	testFile("DATA/PLAYERS.PAL", "Heroes III data files (Data/H3Bitmap.lod) are incomplete or corruped! Please reinstall them.");
+	testFile("SPRITES/DEFAULT.DEF", "Heroes III data files (Data/H3Sprite.lod) are incomplete or corruped! Please reinstall them.");
 
 	srand ( (unsigned int)time(nullptr) );
 
@@ -487,7 +487,8 @@ static void quitApplication()
 	vstd::clear_pointer(CSH);
 	vstd::clear_pointer(VLC);
 
-	vstd::clear_pointer(console);// should be removed after everything else since used by logging
+	// sometimes leads to a hang. TODO: investigate
+	//vstd::clear_pointer(console);// should be removed after everything else since used by logging
 
 	if(!settings["session"]["headless"].Bool())
 		GH.screenHandler().close();
@@ -501,10 +502,16 @@ static void quitApplication()
 
 	std::cout << "Ending...\n";
 
-	// this method is always called from event/network threads, which keep interface mutex locked
-	// unlock it here to avoid assertion failure on GH destruction in exit()
-	GH.interfaceMutex.unlock();
-	exit(0);
+	// Perform quick exit without executing static destructors and let OS cleanup anything that we did not
+	// We generally don't care about them and this leads to numerous issues, e.g.
+	// destruction of locked mutexes (fails an assertion), even in third-party libraries (as well as native libs on Android)
+	// Android - std::quick_exit is available only starting from API level 21
+	// Mingw, macOS and iOS - std::quick_exit is unavailable (at least in current version of CI)
+#if (defined(__ANDROID_API__) && __ANDROID_API__ < 21) || (defined(__MINGW32__)) || defined(VCMI_APPLE)
+	::exit(0);
+#else
+	std::quick_exit(0);
+#endif
 }
 
 void handleQuit(bool ask)

+ 8 - 14
client/adventureMap/CList.cpp

@@ -218,8 +218,7 @@ CHeroList::CEmptyHeroItem::CEmptyHeroItem()
 
 CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero)
 	: CListItem(parent),
-	hero(Hero),
-	parentList(parent)
+	hero(Hero)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	movement = std::make_shared<CAnimImage>(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1);
@@ -285,19 +284,17 @@ void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const
 	const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes[heroPos + 1];
 
 	std::vector<RadialMenuConfig> menuElements = {
-		{ RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [this, heroPos]()
+		{ RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [heroPos]()
 		{
 			for (int i = heroPos; i > 0; i--)
 				LOCPLINT->localState->swapWanderingHero(i, i - 1);
-			parentList->updateWidget();
 		} },
-		{ RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [this, heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); parentList->updateWidget(); } },
-		{ RadialMenuConfig::ITEM_ALT_SW, heroLower != nullptr, "altDown", "vcmi.radialWheel.moveDown", [this, heroPos](){ LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); parentList->updateWidget(); } },
-		{ RadialMenuConfig::ITEM_ALT_SS, heroLower != nullptr, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, heroPos, heroes]()
+		{ RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); } },
+		{ RadialMenuConfig::ITEM_ALT_SW, heroLower != nullptr, "altDown", "vcmi.radialWheel.moveDown", [heroPos](){ LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); } },
+		{ RadialMenuConfig::ITEM_ALT_SS, heroLower != nullptr, "altDownBottom", "vcmi.radialWheel.moveBottom", [heroPos, heroes]()
 		{
 			for (int i = heroPos; i < heroes.size() - 1; i++)
 				LOCPLINT->localState->swapWanderingHero(i, i + 1);
-			parentList->updateWidget();
 		} },
 	};
 
@@ -365,8 +362,7 @@ std::shared_ptr<CIntObject> CTownList::createItem(size_t index)
 }
 
 CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town):
-	CListItem(parent),
-	parentList(parent)
+	CListItem(parent)
 {
 	const std::vector<const CGTownInstance *> towns = LOCPLINT->localState->getOwnedTowns();
 	townIndex = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), Town));
@@ -430,15 +426,13 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const
 		{
 			for (int i = townIndex; i > 0; i--)
 				LOCPLINT->localState->swapOwnedTowns(i, i - 1);
-			parentList->updateWidget();
 		} },
-		{ RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); parentList->updateWidget(); } },
-		{ RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); parentList->updateWidget(); } },
+		{ RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); } },
+		{ RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); } },
 		{ RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, towns]()
 		{
 			for (int i = townIndex; i < towns.size() - 1; i++)
 				LOCPLINT->localState->swapOwnedTowns(i, i + 1);
-			parentList->updateWidget();
 		} },
 	};
 

+ 0 - 2
client/adventureMap/CList.h

@@ -117,7 +117,6 @@ class CHeroList	: public CList
 		std::shared_ptr<CAnimImage> movement;
 		std::shared_ptr<CAnimImage> mana;
 		std::shared_ptr<CAnimImage> portrait;
-		CHeroList *parentList;
 	public:
 		const CGHeroInstance * const hero;
 
@@ -152,7 +151,6 @@ class CTownList	: public CList
 	class CTownItem : public CListItem
 	{
 		std::shared_ptr<CAnimImage> picture;
-		CTownList *parentList;
 	public:
 		int townIndex;
 

+ 2 - 1
client/battle/BattleInterface.cpp

@@ -107,7 +107,8 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
 {
 	auto onIntroPlayed = [this]()
 	{
-		if(LOCPLINT->battleInt)
+		// Make sure that battle have not ended while intro was playing AND that a different one has not started
+		if(LOCPLINT->battleInt.get() == this)
 			onIntroSoundPlayed();
 	};
 

+ 1 - 1
client/lobby/OptionsTab.cpp

@@ -503,7 +503,7 @@ void OptionsTab::SelectionWindow::recreate()
 			int count = 0;
 			for(auto & elem : allowedHeroes)
 			{
-				CHero * type = VLC->heroh->objects[elem];
+				const CHero * type = elem.toHeroType();
 				if(type->heroClass->faction == selectedFaction)
 				{
 					count++;

+ 20 - 3
client/mainmenu/CMainMenu.cpp

@@ -382,12 +382,29 @@ void CMainMenu::openCampaignLobby(std::shared_ptr<CampaignState> campaign)
 
 void CMainMenu::openCampaignScreen(std::string name)
 {
-	if(vstd::contains(CMainMenuConfig::get().getCampaigns().Struct(), name))
+	auto const & config = CMainMenuConfig::get().getCampaigns();
+
+	if(!vstd::contains(config.Struct(), name))
 	{
-		GH.windows().createAndPushWindow<CCampaignScreen>(CMainMenuConfig::get().getCampaigns(), name);
+		logGlobal->error("Unknown campaign set: %s", name);
 		return;
 	}
-	logGlobal->error("Unknown campaign set: %s", name);
+
+	bool campaignsFound = true;
+	for (auto const & entry : config[name]["items"].Vector())
+	{
+		ResourcePath resourceID(entry["file"].String(), EResType::CAMPAIGN);
+		if (!CResourceHandler::get()->existsResource(resourceID))
+			campaignsFound = false;
+	}
+
+	if (!campaignsFound)
+	{
+		CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.client.errors.missingCampaigns"), std::vector<std::shared_ptr<CComponent>>(), PlayerColor(1));
+		return;
+	}
+
+	GH.windows().createAndPushWindow<CCampaignScreen>(config, name);
 }
 
 void CMainMenu::startTutorial()

+ 2 - 3
lib/GameSettings.cpp

@@ -118,11 +118,10 @@ void GameSettings::load(const JsonNode & input)
 
 const JsonNode & GameSettings::getValue(EGameSettings option) const
 {
-	assert(option < EGameSettings::OPTIONS_COUNT);
 	auto index = static_cast<size_t>(option);
 
-	assert(!gameSettings[index].isNull());
-	return gameSettings[index];
+	assert(!gameSettings.at(index).isNull());
+	return gameSettings.at(index);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 20 - 21
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -97,11 +97,7 @@ CObjectClassesHandler::CObjectClassesHandler()
 #undef SET_HANDLER
 }
 
-CObjectClassesHandler::~CObjectClassesHandler()
-{
-	for(auto * p : objects)
-		delete p;
-}
+CObjectClassesHandler::~CObjectClassesHandler() = default;
 
 std::vector<JsonNode> CObjectClassesHandler::loadLegacyData()
 {
@@ -225,6 +221,9 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin
 	return createdObject;
 }
 
+ObjectClass::ObjectClass() = default;
+ObjectClass::~ObjectClass() = default;
+
 std::string ObjectClass::getJsonKey() const
 {
 	return modScope + ':' + identifier;
@@ -240,9 +239,9 @@ std::string ObjectClass::getNameTranslated() const
 	return VLC->generaltexth->translate(getNameTextID());
 }
 
-ObjectClass * CObjectClassesHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index)
+std::unique_ptr<ObjectClass> CObjectClassesHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index)
 {
-	auto * obj = new ObjectClass();
+	auto obj = std::make_unique<ObjectClass>();
 
 	obj->modScope = scope;
 	obj->identifier = name;
@@ -263,31 +262,31 @@ ObjectClass * CObjectClassesHandler::loadFromJson(const std::string & scope, con
 			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, subIndex);
+			loadSubObject(subData.second.meta, subData.first, subData.second, obj.get(), subIndex);
 		}
 		else
-			loadSubObject(subData.second.meta, subData.first, subData.second, obj);
+			loadSubObject(subData.second.meta, subData.first, subData.second, obj.get());
 	}
 
 	if (obj->id == MapObjectID::MONOLITH_TWO_WAY)
-		generateExtraMonolithsForRMG(obj);
+		generateExtraMonolithsForRMG(obj.get());
 
 	return obj;
 }
 
 void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data)
 {
-	auto * object = loadFromJson(scope, data, name, objects.size());
-	objects.push_back(object);
-	VLC->identifiersHandler->registerObject(scope, "object", name, object->id);
+	objects.push_back(loadFromJson(scope, data, name, objects.size()));
+
+	VLC->identifiersHandler->registerObject(scope, "object", name, objects.back()->id);
 }
 
 void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index)
 {
-	auto * object = loadFromJson(scope, data, name, index);
-	assert(objects[(si32)index] == nullptr); // ensure that this id was not loaded before
-	objects[static_cast<si32>(index)] = object;
-	VLC->identifiersHandler->registerObject(scope, "object", name, object->id);
+	assert(objects[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);
 }
 
 void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID)
@@ -299,7 +298,7 @@ void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNo
 		objects[ID.getNum()]->objects.resize(subID.getNum()+1);
 
 	JsonUtils::inherit(config, objects.at(ID.getNum())->base);
-	loadSubObject(config.meta, identifier, config, objects[ID.getNum()], subID.getNum());
+	loadSubObject(config.meta, identifier, config, objects[ID.getNum()].get(), subID.getNum());
 }
 
 void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID)
@@ -335,7 +334,7 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scop
 	std::optional<si32> id = VLC->identifiers()->getIdentifier(scope, "object", type);
 	if(id)
 	{
-		auto * object = objects[id.value()];
+		const auto & object = objects[id.value()];
 		std::optional<si32> subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype);
 
 		if (subID)
@@ -356,7 +355,7 @@ std::set<MapObjectID> CObjectClassesHandler::knownObjects() const
 {
 	std::set<MapObjectID> ret;
 
-	for(auto * entry : objects)
+	for(auto & entry : objects)
 		if (entry)
 			ret.insert(entry->id);
 
@@ -406,7 +405,7 @@ void CObjectClassesHandler::beforeValidate(JsonNode & object)
 
 void CObjectClassesHandler::afterLoadFinalization()
 {
-	for(auto * entry : objects)
+	for(auto & entry : objects)
 	{
 		if (!entry)
 			continue;

+ 6 - 5
lib/mapObjectConstructors/CObjectClassesHandler.h

@@ -45,7 +45,7 @@ class CGObjectInstance;
 using TObjectTypeHandler = std::shared_ptr<AObjectTypeHandler>;
 
 /// Class responsible for creation of adventure map objects of specific type
-class DLL_LINKAGE ObjectClass
+class DLL_LINKAGE ObjectClass : boost::noncopyable
 {
 public:
 	std::string modScope;
@@ -57,7 +57,8 @@ public:
 	JsonNode base;
 	std::vector<TObjectTypeHandler> objects;
 
-	ObjectClass() = default;
+	ObjectClass();
+	~ObjectClass();
 
 	std::string getJsonKey() const;
 	std::string getNameTextID() const;
@@ -65,10 +66,10 @@ public:
 };
 
 /// Main class responsible for creation of all adventure map objects
-class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase
+class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase, boost::noncopyable
 {
 	/// list of object handlers, each of them handles only one type
-	std::vector<ObjectClass * > objects;
+	std::vector< std::unique_ptr<ObjectClass> > objects;
 
 	/// map that is filled during contruction with all known handlers. Not serializeable due to usage of std::function
 	std::map<std::string, std::function<TObjectTypeHandler()> > handlerConstructors;
@@ -82,7 +83,7 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase
 	void loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj);
 	void loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index);
 
-	ObjectClass * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index);
+	std::unique_ptr<ObjectClass> loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index);
 
 	void generateExtraMonolithsForRMG(ObjectClass * container);
 

+ 1 - 1
lib/mapObjectConstructors/CommonConstructors.cpp

@@ -77,7 +77,7 @@ void CTownInstanceConstructor::afterLoadFinalization()
 	{
 		filters[entry.first] = LogicalExpression<BuildingID>(entry.second, [this](const JsonNode & node)
 		{
-			return BuildingID(VLC->identifiers()->getIdentifier("building." + faction->getJsonKey(), node.Vector()[0]).value());
+			return BuildingID(VLC->identifiers()->getIdentifier("building." + faction->getJsonKey(), node.Vector()[0]).value_or(-1));
 		});
 	}
 }

+ 7 - 1
lib/mapObjects/CArmedInstance.cpp

@@ -16,6 +16,7 @@
 #include "../CGeneralTextHandler.h"
 #include "../gameState/CGameState.h"
 #include "../CPlayerState.h"
+#include "../MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -110,7 +111,12 @@ void CArmedInstance::updateMoraleBonusFromArmy()
 	else if (!factions.empty()) // no bonus from empty garrison
 	{
 		b->val = 2 - static_cast<si32>(factionsInArmy);
-		description = boost::str(boost::format(VLC->generaltexth->arraytxt[114]) % factionsInArmy % b->val); //Troops of %d alignments %d
+		MetaString formatter;
+		formatter.appendTextID("core.arraytxt.114"); //Troops of %d alignments %d
+		formatter.replaceNumber(factionsInArmy);
+		formatter.replaceNumber(b->val);
+
+		description = formatter.toString();
 		description = description.substr(0, description.size()-3);//trim value
 	}
 	

+ 1 - 1
lib/mapObjects/CGDwelling.cpp

@@ -160,7 +160,7 @@ void CGDwelling::pickRandomObject(CRandomGenerator & rand)
 		if (subID == MapObjectSubID())
 		{
 			logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level));
-			ID = Obj::CREATURE_GENERATOR4;
+			ID = Obj::CREATURE_GENERATOR1;
 			subID = *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), rand);
 		}
 

+ 83 - 66
lib/mapping/MapFormatH3M.cpp

@@ -1145,42 +1145,47 @@ CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::share
 	auto * object = readGeneric(position, objectTemplate);
 	auto * rewardable = dynamic_cast<CRewardableObject*>(object);
 
-	assert(rewardable);
-
 	// AB and later maps have allowed abilities defined in H3M
 	if(features.levelAB)
 	{
 		std::set<SecondarySkill> allowedAbilities;
 		reader->readBitmaskSkills(allowedAbilities, false);
 
-		if(allowedAbilities.size() != 1)
+		if (rewardable)
 		{
-			auto defaultAllowed = VLC->skillh->getDefaultAllowed();
+			if(allowedAbilities.size() != 1)
+			{
+				auto defaultAllowed = VLC->skillh->getDefaultAllowed();
 
-			for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID)
-				if(defaultAllowed.count(skillID))
-					allowedAbilities.insert(SecondarySkill(skillID));
-		}
+				for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID)
+					if(defaultAllowed.count(skillID))
+						allowedAbilities.insert(SecondarySkill(skillID));
+			}
 
-		JsonNode variable;
-		if (allowedAbilities.size() == 1)
-		{
-			variable.String() = VLC->skills()->getById(*allowedAbilities.begin())->getJsonKey();
+			JsonNode variable;
+			if (allowedAbilities.size() == 1)
+			{
+				variable.String() = VLC->skills()->getById(*allowedAbilities.begin())->getJsonKey();
+			}
+			else
+			{
+				JsonVector anyOfList;
+				for (auto const & skill : allowedAbilities)
+				{
+					JsonNode entry;
+					entry.String() = VLC->skills()->getById(skill)->getJsonKey();
+					anyOfList.push_back(entry);
+				}
+				variable["anyOf"].Vector() = anyOfList;
+			}
+
+			variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods
+			rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable);
 		}
 		else
 		{
-			JsonVector anyOfList;
-			for (auto const & skill : allowedAbilities)
-			{
-				JsonNode entry;
-				entry.String() = VLC->skills()->getById(skill)->getJsonKey();
-				anyOfList.push_back(entry);
-			}
-			variable["anyOf"].Vector() = anyOfList;
+			logGlobal->warn("Failed to set allowed secondary skills to a Witch Hut! Object is not rewardable!");
 		}
-
-		variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods
-		rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable);
 	}
 	return object;
 }
@@ -1201,45 +1206,52 @@ CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared
 	auto bonusType = static_cast<ScholarBonusType>(bonusTypeRaw);
 	auto bonusID = reader->readUInt8();
 
-	switch (bonusType)
+	if (rewardable)
 	{
-		case ScholarBonusType::PRIM_SKILL:
-		{
-			JsonNode variable;
-			JsonNode dice;
-			variable.String() = NPrimarySkill::names[bonusID];
-			variable.setMeta(ModScope::scopeGame());
-			dice.Integer() = 80;
-			rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable);
-			rewardable->configuration.presetVariable("dice", "0", dice);
-			break;
-		}
-		case ScholarBonusType::SECONDARY_SKILL:
-		{
-			JsonNode variable;
-			JsonNode dice;
-			variable.String() = VLC->skills()->getByIndex(bonusID)->getJsonKey();
-			variable.setMeta(ModScope::scopeGame());
-			dice.Integer() = 50;
-			rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable);
-			rewardable->configuration.presetVariable("dice", "0", dice);
-			break;
-		}
-		case ScholarBonusType::SPELL:
+		switch (bonusType)
 		{
-			JsonNode variable;
-			JsonNode dice;
-			variable.String() = VLC->spells()->getByIndex(bonusID)->getJsonKey();
-			variable.setMeta(ModScope::scopeGame());
-			dice.Integer() = 20;
-			rewardable->configuration.presetVariable("spell", "gainedSpell", variable);
-			rewardable->configuration.presetVariable("dice", "0", dice);
-			break;
+			case ScholarBonusType::PRIM_SKILL:
+			{
+				JsonNode variable;
+				JsonNode dice;
+				variable.String() = NPrimarySkill::names[bonusID];
+				variable.setMeta(ModScope::scopeGame());
+				dice.Integer() = 80;
+				rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable);
+				rewardable->configuration.presetVariable("dice", "0", dice);
+				break;
+			}
+			case ScholarBonusType::SECONDARY_SKILL:
+			{
+				JsonNode variable;
+				JsonNode dice;
+				variable.String() = VLC->skills()->getByIndex(bonusID)->getJsonKey();
+				variable.setMeta(ModScope::scopeGame());
+				dice.Integer() = 50;
+				rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable);
+				rewardable->configuration.presetVariable("dice", "0", dice);
+				break;
+			}
+			case ScholarBonusType::SPELL:
+			{
+				JsonNode variable;
+				JsonNode dice;
+				variable.String() = VLC->spells()->getByIndex(bonusID)->getJsonKey();
+				variable.setMeta(ModScope::scopeGame());
+				dice.Integer() = 20;
+				rewardable->configuration.presetVariable("spell", "gainedSpell", variable);
+				rewardable->configuration.presetVariable("dice", "0", dice);
+				break;
+			}
+			case ScholarBonusType::RANDOM:
+				break;// No-op
+			default:
+				logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName);
 		}
-		case ScholarBonusType::RANDOM:
-			break;// No-op
-		default:
-			logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName);
+	}
+	else
+	{
+		logGlobal->warn("Failed to set reward parameters for a Scholar! Object is not rewardable!");
 	}
 
 	reader->skipZero(6);
@@ -1362,16 +1374,21 @@ CGObjectInstance * CMapLoaderH3M::readShrine(const int3 & position, std::shared_
 	auto * object = readGeneric(position, objectTemplate);
 	auto * rewardable = dynamic_cast<CRewardableObject*>(object);
 
-	assert(rewardable);
-
 	SpellID spell = reader->readSpell32();
 
-	if(spell != SpellID::NONE)
+	if (rewardable)
+	{
+		if(spell != SpellID::NONE)
+		{
+			JsonNode variable;
+			variable.String() = VLC->spells()->getById(spell)->getJsonKey();
+			variable.setMeta(ModScope::scopeGame()); // list may include spells from all mods
+			rewardable->configuration.presetVariable("spell", "gainedSpell", variable);
+		}
+	}
+	else
 	{
-		JsonNode variable;
-		variable.String() = VLC->spells()->getById(spell)->getJsonKey();
-		variable.setMeta(ModScope::scopeGame()); // list may include spells from all mods
-		rewardable->configuration.presetVariable("spell", "gainedSpell", variable);
+		logGlobal->warn("Failed to set selected spell to a Shrine!. Object is not rewardable!");
 	}
 	return object;
 }

+ 3 - 0
server/processors/TurnOrderProcessor.cpp

@@ -196,7 +196,10 @@ void TurnOrderProcessor::doStartNewDay()
 	}
 
 	if(!activePlayer)
+	{
 		gameHandler->gameLobby()->setState(EServerState::GAMEPLAY_ENDED);
+		return;
+	}
 
 	std::swap(actedPlayers, awaitingPlayers);