2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'vcmi/beta' into develop

Ivan Savenko 2 жил өмнө
parent
commit
d0b3319f6a

+ 1 - 1
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -84,7 +84,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 				}
 			}
 
-			if(treasureSourcesCount < 5)
+			if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
 				continue;
 
 			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1

+ 43 - 3
ChangeLog.md

@@ -1,10 +1,50 @@
 # 1.3.0 -> 1.3.1
-(unreleased)
 
-* Fixed crash on starting game with outdated mods
-* Fixed Android mod manager crash 
+### GENERAL:
 * Fixed framerate drops on hero movement with active hota mod
+* Fade-out animations will now be skipped when instant hero movement speed is used
+* Restarting loaded campaing scenario will now correctly reapply starting bonus
 * Reverted FPS limit on mobile systems back to 60 fps
+* Fixed loading of translations for maps and campaigns
+* Fixed loading of preconfigured starting army for heroes with preconfigured spells
+* Background battlefield obstacles will now appear below creatures
+* it is now possible to load save game located inside mod
+* Added option to configure reserved screen area in Launcher on iOS
+* Fixed border scrolling when game window is maximized
+
+### AI PLAYER:
+* BattleAI: Improved performance of AI spell selection
+* NKAI: Fixed freeze on attempt to exchange army between garrisoned and visiting hero
+* NKAI: Fixed town threat calculation
+* NKAI: Fixed recruitment of new heroes
+* VCAI: Added workaround to avoid freeze on attempting to reach unreachable location
+* VCAI: Fixed spellcasting by Archangels
+
+### RANDOM MAP GENERATOR:
+* Fixed placement of roads inside rock in underground
+* Fixed placement of shifted creature animations from HotA
+* Fixed placement of treasures at the boundary of wide connections
+* Added more potential locations for quest artifacts in zone
+
+### STABILITY:
+* When starting client without H3 data game will now show message instead of silently crashing
+* When starting invalid map in campaign, game will now show message instead of silently crashing
+* Blocked loading of saves made with different set of mods to prevent crashes
+* Fixed crash on starting game with outdated mods
+* Fixed crash on attempt to sacrifice all your artifacts in Altar of Sacrifice
+* Fixed crash on leveling up after winning battle as defender
+* Fixed possible crash on end of battle opening sound
+* Fixed crash on accepting battle result after winning battle as defender
+* Fixed possible crash on casting spell in battle by AI
+* Fixed multiple possible crashes on managing mods on Android
+* Fixed multiple possible crashes on importing data on Android
+* Fixed crash on refusing rewards from town building
+* Fixed possible crash on threat evaluation by NKAI
+* Fixed crash on using haptic feedback on some Android systems
+* Fixed crash on right-clicking flags area in RMG setup mode
+* Fixed crash on opening Blacksmith window and Build Structure dialogs in some localizations
+* Fixed possible crash on displaying animated main menu
+* Fixed crash on recruiting hero in town located on the border of map
 
 # 1.2.1 -> 1.3.0
 

+ 2 - 0
Global.h

@@ -118,6 +118,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #include <memory>
 #include <mutex>
 #include <numeric>
+#include <optional>
 #include <queue>
 #include <random>
 #include <set>
@@ -126,6 +127,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #include <unordered_map>
 #include <unordered_set>
 #include <utility>
+#include <variant>
 #include <vector>
 
 //The only available version is 3, as of Boost 1.50

+ 1 - 0
README.md

@@ -1,6 +1,7 @@
 [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 # VCMI Project
 VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities.

+ 3 - 0
client/CServerHandler.cpp

@@ -52,6 +52,7 @@
 #include <boost/uuid/uuid.hpp>
 #include <boost/uuid/uuid_io.hpp>
 #include <boost/uuid/uuid_generators.hpp>
+#include <boost/asio.hpp>
 #include "../lib/serializer/Cast.h"
 #include "LobbyClientNetPackVisitors.h"
 
@@ -86,6 +87,8 @@ template<typename T> class CApplyOnLobby : public CBaseForLobbyApply
 public:
 	bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override
 	{
+		boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+
 		T * ptr = static_cast<T *>(pack);
 		ApplyOnLobbyHandlerNetPackVisitor visitor(*handler);
 

+ 3 - 3
client/adventureMap/AdventureMapInterface.cpp

@@ -169,10 +169,10 @@ void AdventureMapInterface::tick(uint32_t msPassed)
 void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed)
 {
 	/// Width of window border, in pixels, that triggers map scrolling
-	static constexpr uint32_t borderScrollWidth = 15;
+	static constexpr int32_t borderScrollWidth = 15;
 
-	uint32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float();
-	uint32_t scrollDistance = scrollSpeedPixels * timePassed / 1000;
+	int32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float();
+	int32_t scrollDistance = scrollSpeedPixels * timePassed / 1000;
 
 	Point cursorPosition = GH.getCursorPosition();
 	Point scrollDirection;

+ 1 - 1
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -52,7 +52,7 @@
 	</categories>
 	<releases>
 		<release version="1.4.0" date="2023-12-22" type="development" />
-		<release version="1.3.1" date="2023-08-18" type="development" />
+		<release version="1.3.1" date="2023-08-18" />
 		<release version="1.3.0" date="2023-08-04" />
 		<release version="1.2.1" date="2023-04-28" />
 		<release version="1.2.0" date="2023-04-14" />

+ 4 - 0
lib/battle/CBattleInfoCallback.cpp

@@ -1731,6 +1731,10 @@ SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const
 	TConstBonusListPtr bl = caster->getBonuses(Selector::type()(BonusType::SPELLCASTER));
 	if (!bl->size())
 		return SpellID::NONE;
+
+	if(bl->size() == 1)
+		return SpellID(bl->front()->subtype);
+
 	int totalWeight = 0;
 	for(const auto & b : *bl)
 	{

+ 35 - 9
lib/modding/CModHandler.cpp

@@ -489,23 +489,49 @@ void CModHandler::afterLoad(bool onlyEssential)
 
 }
 
-void CModHandler::trySetActiveMods(const std::map<TModID, CModVersion> & modList)
+void CModHandler::trySetActiveMods(std::vector<TModID> saveActiveMods, const std::map<TModID, CModVersion> & modList)
 {
+	std::vector<TModID> newActiveMods;
+
 	ModIncompatibility::ModList missingMods;
 
-	for(const auto & mod : modList)
+	for(const auto & m : activeMods)
+	{
+		if (vstd::contains(saveActiveMods, m))
+			continue;
+
+		auto & modInfo = allMods.at(m);
+		if(modInfo.checkModGameplayAffecting())
+			missingMods.emplace_back(m, modInfo.version.toString());
+	}
+
+	for(const auto & m : saveActiveMods)
 	{
-		auto m = mod.first;
-		auto mver = mod.second;
+		const CModVersion & mver = modList.at(m);
+
+		if (allMods.count(m) == 0)
+		{
+			missingMods.emplace_back(m, mver.toString());
+			continue;
+		}
+
+		auto & modInfo = allMods.at(m);
+
+		bool modAffectsGameplay = modInfo.checkModGameplayAffecting();
+		bool modVersionCompatible = modInfo.version.isNull() || mver.isNull() || modInfo.version.compatible(mver);
+		bool modEnabledLocally = vstd::contains(activeMods, m);
+		bool modCanBeEnabled = modEnabledLocally && modVersionCompatible;
+
+		allMods[m].setEnabled(modCanBeEnabled);
+
+		if (modCanBeEnabled)
+			newActiveMods.push_back(m);
 
-		if(allMods.count(m) && (allMods[m].version.isNull() || mver.isNull() || allMods[m].version.compatible(mver)))
-			allMods[m].setEnabled(true);
-		else
+		if (!modCanBeEnabled && modAffectsGameplay)
 			missingMods.emplace_back(m, mver.toString());
 	}
 
-	if(!missingMods.empty())
-		throw ModIncompatibility(std::move(missingMods));
+	std::swap(activeMods, newActiveMods);
 }
 
 CIdentifierStorage & CModHandler::getIdentifiers()

+ 5 - 7
lib/modding/CModHandler.h

@@ -52,7 +52,7 @@ class DLL_LINKAGE CModHandler : boost::noncopyable
 	CModVersion getModVersion(TModID modName) const;
 
 	/// Attempt to set active mods according to provided list of mods from save, throws on failure
-	void trySetActiveMods(const std::map<TModID, CModVersion> & modList);
+	void trySetActiveMods(std::vector<TModID> saveActiveMods, const std::map<TModID, CModVersion> & modList);
 
 	std::unique_ptr<CIdentifierStorage> identifiers;
 
@@ -100,16 +100,14 @@ public:
 		else
 		{
 			loadMods();
-			std::vector<TModID> newActiveMods;
+			std::vector<TModID> saveActiveMods;
 			std::map<TModID, CModVersion> modVersions;
-			h & newActiveMods;
+			h & saveActiveMods;
 
-			for(const auto & m : newActiveMods)
+			for(const auto & m : saveActiveMods)
 				h & modVersions[m];
 
-			trySetActiveMods(modVersions);
-
-			std::swap(activeMods, newActiveMods);
+			trySetActiveMods(saveActiveMods, modVersions);
 		}
 
 		h & identifiers;

+ 43 - 0
lib/modding/CModInfo.cpp

@@ -12,6 +12,7 @@
 
 #include "../CGeneralTextHandler.h"
 #include "../VCMI_Lib.h"
+#include "../filesystem/Filesystem.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -128,6 +129,48 @@ void CModInfo::loadLocalData(const JsonNode & data)
 		validation = validated ? PASSED : FAILED;
 }
 
+bool CModInfo::checkModGameplayAffecting() const
+{
+	if (modGameplayAffecting.has_value())
+		return *modGameplayAffecting;
+
+	static const std::vector<std::string> keysToTest = {
+		"heroClasses",
+		"artifacts",
+		"creatures",
+		"factions",
+		"objects",
+		"heroes",
+		"spells",
+		"skills",
+		"templates",
+		"scripts",
+		"battlefields",
+		"terrains",
+		"rivers",
+		"roads",
+		"obstacles"
+	};
+
+	ResourceID modFileResource(CModInfo::getModFile(identifier));
+
+	if(CResourceHandler::get("initial")->existsResource(modFileResource))
+	{
+		const JsonNode modConfig(modFileResource);
+
+		for(const auto & key : keysToTest)
+		{
+			if (!modConfig[key].isNull())
+			{
+				modGameplayAffecting = true;
+				return *modGameplayAffecting;
+			}
+		}
+	}
+	modGameplayAffecting = false;
+	return *modGameplayAffecting;
+}
+
 bool CModInfo::isEnabled() const
 {
 	return implicitlyEnabled && explicitlyEnabled;

+ 7 - 0
lib/modding/CModInfo.h

@@ -18,6 +18,10 @@ using TModID = std::string;
 
 class DLL_LINKAGE CModInfo
 {
+	/// cached result of checkModGameplayAffecting() call
+	/// Do not serialize - depends on local mod version, not server/save mod version
+	mutable std::optional<bool> modGameplayAffecting;
+
 public:
 	enum EValidationStatus
 	{
@@ -67,6 +71,9 @@ public:
 	static std::string getModDir(const std::string & name);
 	static std::string getModFile(const std::string & name);
 
+	/// return true if this mod can affect gameplay, e.g. adds or modifies any game objects
+	bool checkModGameplayAffecting() const;
+
 private:
 	/// true if mod is enabled by user, e.g. in Launcher UI
 	bool explicitlyEnabled;

+ 32 - 14
lib/rmg/modificators/ObjectManager.cpp

@@ -413,23 +413,41 @@ bool ObjectManager::createRequiredObjects()
 		
 		zone.connectPath(path);
 		placeObject(rmgObject, guarded, true);
-		
-		for(const auto & nearby : nearbyObjects)
+	}
+
+	for(const auto & nearby : nearbyObjects)
+	{
+		auto * targetObject = nearby.nearbyTarget;
+		if (!targetObject || !targetObject->appearance)
 		{
-			if(nearby.nearbyTarget != objInfo.obj)
-				continue;
-			
-			rmg::Object rmgNearObject(*nearby.obj);
-			rmg::Area possibleArea(rmgObject.instances().front()->getBlockedArea().getBorderOutside());
-			possibleArea.intersect(zone.areaPossible());
-			if(possibleArea.empty())
+			continue;
+		}
+
+		rmg::Object rmgNearObject(*nearby.obj);
+		rmg::Area possibleArea(rmg::Area(targetObject->getBlockedPos()).getBorderOutside());
+		possibleArea.intersect(zone.areaPossible());
+		if(possibleArea.empty())
+		{
+			rmgNearObject.clear();
+			continue;
+		}
+
+		rmgNearObject.setPosition(*RandomGeneratorUtil::nextItem(possibleArea.getTiles(), zone.getRand()));
+		placeObject(rmgNearObject, false, false);
+		auto path = zone.searchPath(rmgNearObject.getVisitablePosition(), false);
+		if (path.valid())
+		{
+			zone.connectPath(path);
+		}
+		else
+		{
+			for (auto* instance : rmgNearObject.instances())
 			{
-				rmgNearObject.clear();
-				continue;
+				logGlobal->error("Failed to connect nearby object %s at %s",
+					instance->object().getObjectName(), instance->getPosition(true).toString());
+				mapProxy->removeObject(&instance->object());
 			}
-			
-			rmgNearObject.setPosition(*RandomGeneratorUtil::nextItem(possibleArea.getTiles(), zone.getRand()));
-			placeObject(rmgNearObject, false, false);
+			rmgNearObject.clear();
 		}
 	}
 	

+ 0 - 1
server/CGameHandler.cpp

@@ -77,7 +77,6 @@
 #define COMPLAIN_RETF(txt, FORMAT) {complain(boost::str(boost::format(txt) % FORMAT)); return false;}
 
 CondSh<bool> battleMadeAction(false);
-boost::recursive_mutex battleActionMutex;
 CondSh<BattleResult *> battleResult(nullptr);
 template <typename T> class CApplyOnGH;
 

+ 2 - 0
server/CGameHandler.h

@@ -102,6 +102,8 @@ class CGameHandler : public IGameCallback, public CBattleInfoCallback, public En
 	std::unique_ptr<boost::thread> battleThread;
 
 public:
+	boost::recursive_mutex battleActionMutex;
+
 	std::unique_ptr<HeroPoolProcessor> heroPool;
 
 	using FireShieldInfo = std::vector<std::pair<const CStack *, int64_t>>;

+ 1 - 1
server/HeroPoolProcessor.cpp

@@ -244,7 +244,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy
 	hr.hid = recruitedHero->subID;
 	hr.player = player;
 	hr.tile = recruitedHero->convertFromVisitablePos(targetPos );
-	if(gameHandler->getTile(hr.tile)->isWater() && !recruitedHero->boat)
+	if(gameHandler->getTile(targetPos)->isWater() && !recruitedHero->boat)
 	{
 		//Create a new boat for hero
 		gameHandler->createObject(targetPos , Obj::BOAT, recruitedHero->getBoatType().getNum());

+ 2 - 4
server/NetPacksServer.cpp

@@ -25,8 +25,6 @@
 #include "../lib/spells/ISpellMechanics.h"
 #include "../lib/serializer/Cast.h"
 
-extern boost::recursive_mutex battleActionMutex;
-
 void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack)
 {
 	gh.save(pack.fname);
@@ -282,7 +280,7 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack)
 
 void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack)
 {
-	boost::unique_lock lock(battleActionMutex);
+	boost::unique_lock lock(gh.battleActionMutex);
 
 	const BattleInfo * b = gs.curB;
 	if(!b)
@@ -311,7 +309,7 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack)
 
 void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack)
 {
-	boost::unique_lock lock(battleActionMutex);
+	boost::unique_lock lock(gh.battleActionMutex);
 
 	const BattleInfo * b = gs.curB;
 	if(!b)