diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 581c725e6..8f9b80f6d 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/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 diff --git a/ChangeLog.md b/ChangeLog.md index 3ba074a01..31a09a49b 100644 --- a/ChangeLog.md +++ b/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 diff --git a/Global.h b/Global.h index dda61c804..3c308f54c 100644 --- a/Global.h +++ b/Global.h @@ -118,6 +118,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include +#include #include #include #include @@ -126,6 +127,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include +#include #include //The only available version is 3, as of Boost 1.50 diff --git a/README.md b/README.md index 5c71fb47d..05ff3fd27 100644 --- a/README.md +++ b/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. diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index e154484f1..5281cfaa5 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include "../lib/serializer/Cast.h" #include "LobbyClientNetPackVisitors.h" @@ -86,6 +87,8 @@ template class CApplyOnLobby : public CBaseForLobbyApply public: bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override { + boost::unique_lock un(*CPlayerInterface::pim); + T * ptr = static_cast(pack); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 45ea69a63..7a8d4b3ea 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/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; diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 9ac93bd8d..249adc947 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -52,7 +52,7 @@ - + diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index d0990a481..23c20abe3 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/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) { diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 6857a4381..010a9a4b5 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -489,23 +489,49 @@ void CModHandler::afterLoad(bool onlyEssential) } -void CModHandler::trySetActiveMods(const std::map & modList) +void CModHandler::trySetActiveMods(std::vector saveActiveMods, const std::map & modList) { + std::vector newActiveMods; + ModIncompatibility::ModList missingMods; - for(const auto & mod : modList) + for(const auto & m : activeMods) { - auto m = mod.first; - auto mver = mod.second; + if (vstd::contains(saveActiveMods, m)) + continue; - if(allMods.count(m) && (allMods[m].version.isNull() || mver.isNull() || allMods[m].version.compatible(mver))) - allMods[m].setEnabled(true); - else + auto & modInfo = allMods.at(m); + if(modInfo.checkModGameplayAffecting()) + missingMods.emplace_back(m, modInfo.version.toString()); + } + + for(const auto & m : saveActiveMods) + { + 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 (!modCanBeEnabled && modAffectsGameplay) missingMods.emplace_back(m, mver.toString()); } - if(!missingMods.empty()) - throw ModIncompatibility(std::move(missingMods)); + std::swap(activeMods, newActiveMods); } CIdentifierStorage & CModHandler::getIdentifiers() diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index d782bfa18..eaa84d84a 100644 --- a/lib/modding/CModHandler.h +++ b/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 & modList); + void trySetActiveMods(std::vector saveActiveMods, const std::map & modList); std::unique_ptr identifiers; @@ -100,16 +100,14 @@ public: else { loadMods(); - std::vector newActiveMods; + std::vector saveActiveMods; std::map 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; diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index f8d75fb48..7b6d66f25 100644 --- a/lib/modding/CModInfo.cpp +++ b/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 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; diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index f78db9336..72e60f01a 100644 --- a/lib/modding/CModInfo.h +++ b/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 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; diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index d1afd557e..02267aaa8 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/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(); } } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 748ef73ee..68a907cd9 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -77,7 +77,6 @@ #define COMPLAIN_RETF(txt, FORMAT) {complain(boost::str(boost::format(txt) % FORMAT)); return false;} CondSh battleMadeAction(false); -boost::recursive_mutex battleActionMutex; CondSh battleResult(nullptr); template class CApplyOnGH; diff --git a/server/CGameHandler.h b/server/CGameHandler.h index a3f906408..49a6dfcd6 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -102,6 +102,8 @@ class CGameHandler : public IGameCallback, public CBattleInfoCallback, public En std::unique_ptr battleThread; public: + boost::recursive_mutex battleActionMutex; + std::unique_ptr heroPool; using FireShieldInfo = std::vector>; diff --git a/server/HeroPoolProcessor.cpp b/server/HeroPoolProcessor.cpp index 4c92e5963..f32a88be0 100644 --- a/server/HeroPoolProcessor.cpp +++ b/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()); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 7660bc073..3027242c0 100644 --- a/server/NetPacksServer.cpp +++ b/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)