From cbe1c4a07dd3cedf728d5356cefda4f7cb689374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 3 Jun 2023 08:53:00 +0200 Subject: [PATCH 001/168] Remove random seed from map description. --- lib/rmg/CMapGenerator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 2dafe736b..9417b9dd5 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -164,9 +164,9 @@ std::string CMapGenerator::getMapDescription() const throw rmgException("Map template for Random Map Generator is not found. Could not start the game."); std::stringstream ss; - ss << boost::str(boost::format(std::string("Map created by the Random Map Generator.\nTemplate was %s, Random seed was %d, size %dx%d") + + ss << boost::str(boost::format(std::string("Map created by the Random Map Generator.\nTemplate was %s, size %dx%d") + ", levels %d, players %d, computers %d, water %s, monster %s, VCMI map") % mapTemplate->getName() % - randomSeed % map->width() % map->height() % static_cast(map->levels()) % static_cast(mapGenOptions.getPlayerCount()) % + map->width() % map->height() % static_cast(map->levels()) % static_cast(mapGenOptions.getPlayerCount()) % static_cast(mapGenOptions.getCompOnlyPlayerCount()) % waterContentStr[mapGenOptions.getWaterContent()] % monsterStrengthStr[monsterStrengthIndex]); From 8bfa34e97b1f8a2b96b7e5786386713def949b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Fri, 21 Jul 2023 14:55:34 +0200 Subject: [PATCH 002/168] Allow routing road behind objects not visitable from top if that's absolutely neccessary. --- lib/rmg/modificators/RoadPlacer.cpp | 56 +++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index e78808cfc..908e3d57d 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -54,22 +54,52 @@ const rmg::Area & RoadPlacer::getRoads() const bool RoadPlacer::createRoad(const int3 & dst) { - auto searchArea = zone.areaPossible() + areaRoads + zone.freePaths() - isolated + roads; - + auto searchArea = zone.areaPossible() + zone.freePaths() + areaRoads + roads; + rmg::Path path(searchArea); path.connect(roads); + + auto simpleRoutig = [this](const int3& src, const int3& dst) + { + if(areaIsolated().contains(dst)) + { + return 1000.0f; //Do not route road behind objects that are not visitable from top + } + else + { + return 1.0f; + } + }; - auto res = path.search(dst, true); + auto res = path.search(dst, true, simpleRoutig); if(!res.valid()) { - res = path.search(dst, false, [](const int3 & src, const int3 & dst) + auto desperateRoutig = [this](const int3& src, const int3& dst) -> float { + //Do not allow connections straight up through object not visitable from top + if(std::abs((src - dst).y) == 1) + { + if(areaIsolated().contains(dst) || areaIsolated().contains(src)) + { + return 1e30; + } + } + else + { + if(areaIsolated().contains(dst)) + { + return 1e6; + } + } + float weight = dst.dist2dSQ(src); return weight * weight; - }); + }; + res = path.search(dst, false, desperateRoutig); + if(!res.valid()) { - logGlobal->warn("Failed to create road"); + logGlobal->warn("Failed to create road to node %s", dst.toString()); return false; } } @@ -149,7 +179,19 @@ void RoadPlacer::connectRoads() for(const auto & node : roadNodes) { - createRoad(node); + try + { + createRoad(node); + } + catch (const rmgException& e) + { + logGlobal->warn("Handled exception while drawing road to node %s: %s", node.toString(), e.what()); + } + catch (const std::exception & e) + { + logGlobal->error("Unhandled exception while drawing road to node %s: %s", node.toString(), e.what()); + throw e; + } } //Draw dirt roads if there are only mines From 1762df2b775c405cc9f6c3c7892a46ff983b4c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Fri, 21 Jul 2023 14:55:49 +0200 Subject: [PATCH 003/168] Formatting --- lib/rmg/modificators/ObjectManager.cpp | 1 + lib/rmg/modificators/RoadPlacer.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 2e75c38a1..5b8136a01 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -464,6 +464,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD objects.push_back(&instance->object()); if(auto * m = zone.getModificator()) { + //FIXME: Objects that can be removed, can be trespassed. Does not include Corpse if(instance->object().appearance->isVisitableFromTop()) m->areaForRoads().add(instance->getVisitablePosition()); else diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index 908e3d57d..498c4fab0 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -31,7 +31,7 @@ void RoadPlacer::process() void RoadPlacer::init() { - if (zone.isUnderground()) + if(zone.isUnderground()) { DEPENDENCY_ALL(RockFiller); } @@ -118,7 +118,7 @@ void RoadPlacer::drawRoads(bool secondary) zone.freePaths().unite(roads); } - if (!generator.getMapGenOptions().isRoadEnabled()) + if(!generator.getMapGenOptions().isRoadEnabled()) { return; } @@ -138,7 +138,7 @@ void RoadPlacer::drawRoads(bool secondary) //If our road type is not enabled, choose highest below it for (int8_t bestRoad = roadType.getNum(); bestRoad > RoadId(Road::NO_ROAD).getNum(); bestRoad--) { - if (generator.getMapGenOptions().isRoadEnabled(RoadId(bestRoad))) + if(generator.getMapGenOptions().isRoadEnabled(RoadId(bestRoad))) { mapProxy->drawRoads(zone.getRand(), tiles, RoadId(bestRoad)); return; @@ -156,11 +156,11 @@ void RoadPlacer::connectRoads() { bool noRoadNodes = false; //Assumes objects are already placed - if (roadNodes.size() < 2) + if(roadNodes.size() < 2) { //If there are no nodes, draw roads to mines noRoadNodes = true; - if (auto* m = zone.getModificator()) + if(auto* m = zone.getModificator()) { for(auto * object : m->getMines()) { From d5e49a02c13caf556cd26e74b8a8882cf5fbe5fd Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 21 Jul 2023 16:19:16 +0300 Subject: [PATCH 004/168] Use full debug information in release build on Android --- android/vcmi-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 6f21bbe37..81554e855 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -42,7 +42,7 @@ android { applicationLabel: '@string/app_name', ] ndk { - debugSymbolLevel 'symbol_table' + debugSymbolLevel 'full' } } daily { From 83daef8362d0d6b753911290b6ef5f6b9a6cf683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Fri, 21 Jul 2023 17:28:11 +0200 Subject: [PATCH 005/168] Rename default map template to random map template --- Mods/vcmi/config/vcmi/english.json | 2 +- Mods/vcmi/config/vcmi/polish.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 890101689..f7d9be64c 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -179,7 +179,7 @@ "vcmi.questLog.hideComplete.hover" : "Hide complete quests", "vcmi.questLog.hideComplete.help" : "Hide all completed quests", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(default)", + "vcmi.randomMapTab.widgets.defaultTemplate" : "(Random)", "vcmi.randomMapTab.widgets.templateLabel" : "Template", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index b8e414826..f41383471 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -168,7 +168,7 @@ "vcmi.questLog.hideComplete.hover" : "Ukryj ukończone misje", "vcmi.questLog.hideComplete.help" : "Ukrywa wszystkie misje, które zostały zakończone", - "vcmi.randomMapTab.widgets.defaultTemplate" : "domyślny", + "vcmi.randomMapTab.widgets.defaultTemplate" : "(Losowy)", "vcmi.randomMapTab.widgets.templateLabel" : "Szablon", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Ustaw...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Sojusze", From e6d8b65e2c170ea8f0a19fd540f016d0a47d4989 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 19 Jul 2023 15:15:23 +0300 Subject: [PATCH 006/168] Fix toggling of auto combat --- client/CPlayerInterface.cpp | 3 ++- client/battle/BattleSiegeController.cpp | 2 -- client/battle/BattleStacksController.cpp | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 445359148..9a2c0e41c 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1904,8 +1904,9 @@ bool CPlayerInterface::capturedAllEvents() } bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations(); + bool quickCombatOngoing = isAutoFightOn && !battleInt; - if (ignoreEvents || needToLockAdventureMap || isAutoFightOn) + if (ignoreEvents || needToLockAdventureMap || quickCombatOngoing ) { GH.input().ignoreEventsUntilInput(); return true; diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 64e023834..46852849c 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -330,8 +330,6 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) { - owner.checkForAnimations(); - if (ca.attacker != -1) { const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker); diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 0a068d274..55d416af3 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -154,11 +154,6 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer) void BattleStacksController::stackReset(const CStack * stack) { - owner.checkForAnimations(); - - //reset orientation? - //stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER; - auto iter = stackAnimation.find(stack->unitId()); if(iter == stackAnimation.end()) From a912770e2927ef09dee195e0e294c49f5c260df6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 22 Jul 2023 12:33:05 +0300 Subject: [PATCH 007/168] Fix activation of radial menu on touchscreen --- client/eventsSDL/InputSourceTouch.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index 31274e0dd..5d0827dd9 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -81,7 +81,10 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin { Point distance = convertTouchToMouse(tfinger) - lastTapPosition; if ( std::abs(distance.x) > params.panningSensitivityThreshold || std::abs(distance.y) > params.panningSensitivityThreshold) + { state = TouchState::TAP_DOWN_PANNING; + GH.events().dispatchGesturePanningStarted(lastTapPosition); + } break; } case TouchState::TAP_DOWN_PANNING: @@ -128,7 +131,6 @@ void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinge { lastTapPosition = convertTouchToMouse(tfinger); GH.input().setCursorPosition(lastTapPosition); - GH.events().dispatchGesturePanningStarted(lastTapPosition); state = TouchState::TAP_DOWN_SHORT; break; } From 4ce774a380ebaa0ac7060cd710ef562627d62ca5 Mon Sep 17 00:00:00 2001 From: krs Date: Sat, 22 Jul 2023 13:42:57 +0300 Subject: [PATCH 008/168] Wrong placement of onlyOnWaterMap in spells schema. --- config/schemas/obstacle.json | 4 ++-- config/schemas/spell.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/schemas/obstacle.json b/config/schemas/obstacle.json index d96e75410..780f224f9 100644 --- a/config/schemas/obstacle.json +++ b/config/schemas/obstacle.json @@ -30,11 +30,11 @@ }, "width" : { "type" : "number", - "description" : "Width ob obstacle" + "description" : "Width of obstacle" }, "height" : { "type" : "number", - "description" : "height if obstacle" + "description" : "height of obstacle" }, "blockedTiles" : { "type" : "array", diff --git a/config/schemas/spell.json b/config/schemas/spell.json index 28caae4a2..7de694c37 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -308,10 +308,10 @@ "$ref" : "#/definitions/levelInfo" } } + }, + "onlyOnWaterMap" : { + "type" : "boolean", + "description" : "It true, spell won't be available on a map without water" } }, - "onlyOnWaterMap" : { - "type" : "boolean", - "description" : "It true, spell won't be available on a map without water" - } } From 59f3740aeab31c76852d110333a4684e24fa57cd Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 22 Jul 2023 13:17:16 +0200 Subject: [PATCH 009/168] allow clipboard paste --- client/eventsSDL/InputSourceKeyboard.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index 7c953063c..6fd1c5bb3 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -17,6 +17,7 @@ #include "../gui/EventDispatcher.h" #include "../gui/ShortcutHandler.h" +#include #include #include @@ -30,13 +31,21 @@ InputSourceKeyboard::InputSourceKeyboard() void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) { - if(key.repeat != 0) - return; // ignore periodic event resends - if (SDL_IsTextInputActive() == SDL_TRUE) { + if(key.keysym.sym == SDLK_v) { + std::string clipboardContent = SDL_GetClipboardText(); + boost::erase_all(clipboardContent, "\r"); + boost::erase_all(clipboardContent, "\n"); + GH.events().dispatchTextInput(clipboardContent); + return; + } + if (key.keysym.sym >= ' ' && key.keysym.sym < 0x80) return; // printable character - will be handled as text input + } else { + if(key.repeat != 0) + return; // ignore periodic event resends } assert(key.state == SDL_PRESSED); From 746f8c5e62734ec6ceae3e56a00ad562d74dd6bf Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 22 Jul 2023 14:26:37 +0200 Subject: [PATCH 010/168] added missing check for ctrl --- client/eventsSDL/InputSourceKeyboard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index 6fd1c5bb3..abc30da0c 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -33,7 +33,7 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) { if (SDL_IsTextInputActive() == SDL_TRUE) { - if(key.keysym.sym == SDLK_v) { + if(key.keysym.sym == SDLK_v && isKeyboardCtrlDown()) { std::string clipboardContent = SDL_GetClipboardText(); boost::erase_all(clipboardContent, "\r"); boost::erase_all(clipboardContent, "\n"); From e8771e40a58b8f40463a989d3aa84f7a1c4ba8ee Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 22 Jul 2023 14:36:13 +0200 Subject: [PATCH 011/168] Update client/eventsSDL/InputSourceKeyboard.cpp Co-authored-by: Ivan Savenko --- client/eventsSDL/InputSourceKeyboard.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index abc30da0c..618372aba 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -33,12 +33,15 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) { if (SDL_IsTextInputActive() == SDL_TRUE) { - if(key.keysym.sym == SDLK_v && isKeyboardCtrlDown()) { - std::string clipboardContent = SDL_GetClipboardText(); - boost::erase_all(clipboardContent, "\r"); - boost::erase_all(clipboardContent, "\n"); - GH.events().dispatchTextInput(clipboardContent); - return; + if(key.keysym.sym == SDLK_v && isKeyboardCtrlDown()) + { + char * clipboardBuffer = SDL_GetClipboardText(); + std::string clipboardContent = clipboardBuffer; + boost::erase_all(clipboardContent, "\r"); + boost::erase_all(clipboardContent, "\n"); + GH.events().dispatchTextInput(clipboardContent); + SDL_free(clipboardBuffer); + return; } if (key.keysym.sym >= ' ' && key.keysym.sym < 0x80) From 05735a20e128179604721d944b0aedeb656e7322 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sat, 22 Jul 2023 15:32:00 +0200 Subject: [PATCH 012/168] Fix for hero info window mana points not getting spent on spellcast --- client/battle/BattleInterface.cpp | 8 ++++++++ client/battle/BattleInterfaceClasses.cpp | 14 ++++++++++++++ client/battle/BattleInterfaceClasses.h | 3 +++ client/battle/BattleWindow.cpp | 10 ++++++++-- client/battle/BattleWindow.h | 3 +++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 53fb5f3c7..c7e5cb07c 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -584,7 +584,15 @@ void BattleInterface::endAction(const BattleAction* action) //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed if(action->actionType == EActionType::HERO_SPELL) + { fieldController->redrawBackgroundWithHexes(); + + //update casting hero info window + auto hero = action->side == 0 ? attackingHero : defendingHero; + InfoAboutHero heroInfo = InfoAboutHero(); + heroInfo.initFromHero(hero->instance(), InfoAboutHero::INBATTLE); + windowObject->updateHeroInfoWindow(action->side, heroInfo); + } } void BattleInterface::appendBattleLog(const std::string & newEntry) diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 68dbd9a3a..a6289d07f 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -389,6 +389,12 @@ HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * posit background->colorize(hero.owner); } + initializeData(hero); +} + +void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; auto attack = hero.details->primskills[0]; auto defense = hero.details->primskills[1]; auto power = hero.details->primskills[2]; @@ -423,6 +429,14 @@ HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * posit labels.push_back(std::make_shared(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints))); } +void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo) +{ + icons.clear(); + labels.clear(); + + initializeData(updatedInfo); +} + void HeroInfoBasicPanel::show(Canvas & to) { showAll(to); diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index 5f730d56e..aa5668bc5 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -137,6 +137,9 @@ public: HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true); void show(Canvas & to) override; + + void initializeData(const InfoAboutHero & hero); + void update(const InfoAboutHero & updatedInfo); }; class HeroInfoWindow : public CWindowObject diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 4fe763e04..1d0e314d1 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -237,8 +237,8 @@ void BattleWindow::showStickyHeroWindows() if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true) return; - Settings showStickyHeroInfoWIndows = settings.write["battle"]["stickyHeroInfoWindows"]; - showStickyHeroInfoWIndows->Bool() = true; + Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; + showStickyHeroInfoWindows->Bool() = true; createStickyHeroInfoWindows(); @@ -250,6 +250,12 @@ void BattleWindow::updateQueue() queue->update(); } +void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero) +{ + std::shared_ptr panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow; + panelToUpdate->update(hero); +} + void BattleWindow::activate() { GH.setStatusbar(console); diff --git a/client/battle/BattleWindow.h b/client/battle/BattleWindow.h index 0b37f1e1e..a16d913ca 100644 --- a/client/battle/BattleWindow.h +++ b/client/battle/BattleWindow.h @@ -88,6 +88,9 @@ public: /// Refresh queue after turn order changes void updateQueue(); + /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side + void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero); + /// Get mouse-hovered battle queue unit ID if any found std::optional getQueueHoveredUnitId(); From 5aa2492a90f3e47df7648cfd9fc609b9c907a71c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 22 Jul 2023 22:04:12 +0300 Subject: [PATCH 013/168] Fixed crash on ending battle with a draw --- lib/NetPacksLib.cpp | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index cfc73a548..c2c3880a1 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2205,25 +2205,31 @@ void BattleResultAccepted::applyGs(CGameState * gs) const for(auto & res : heroResult) { if(res.hero) - res.hero->removeBonusesRecursive(Bonus::OneBattle); - } - - // Grow up growing artifacts - if(const auto hero = heroResult[winnerSide].hero) - { - if(hero->commander && hero->commander->alive) - { - for(auto & art : hero->commander->artifactsWorn) - art.second.artifact->growingUp(); - } - for(auto & art : hero->artifactsWorn) - { - art.second.artifact->growingUp(); - } - } - if(VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - { - if(heroResult[0].army) + res.hero->removeBonusesRecursive(Bonus::OneBattle); + } + + if(winnerSide != 2) + { + // Grow up growing artifacts + const auto hero = heroResult[winnerSide].hero; + + if (hero) + { + if(hero->commander && hero->commander->alive) + { + for(auto & art : hero->commander->artifactsWorn) + art.second.artifact->growingUp(); + } + for(auto & art : hero->artifactsWorn) + { + art.second.artifact->growingUp(); + } + } + } + + if(VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + { + if(heroResult[0].army) heroResult[0].army->giveStackExp(heroResult[0].exp); if(heroResult[1].army) heroResult[1].army->giveStackExp(heroResult[1].exp); From a619193e3c2136cc15721e7a2b4452b711c5cf94 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 22 Jul 2023 22:04:32 +0300 Subject: [PATCH 014/168] Fixed initialization of heroes in taverns --- lib/NetPacksLib.cpp | 69 ++++++++++++------------------- lib/mapObjects/CGHeroInstance.cpp | 31 +++++++------- 2 files changed, 40 insertions(+), 60 deletions(-) diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index c2c3880a1..b1810d343 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1390,18 +1390,7 @@ void HeroRecruited::applyGs(CGameState * gs) const h->setOwner(player); h->pos = tile; - bool fresh = !h->isInitialized(); - if(fresh) - { // this is a fresh hero who hasn't appeared yet - if (boatId >= 0) //Hero spawns on water - { - h->setMovementPoints(h->movementPointsLimit(false)); - } - else - { - h->setMovementPoints(h->movementPointsLimit(true)); - } - } + h->initObj(gs->getRandomGenerator()); if(h->id == ObjectInstanceID()) { @@ -1414,16 +1403,10 @@ void HeroRecruited::applyGs(CGameState * gs) const gs->map->heroesOnMap.emplace_back(h); p->heroes.emplace_back(h); h->attachTo(*p); - if(fresh) - { - h->initObj(gs->getRandomGenerator()); - } gs->map->addBlockVisTiles(h); if(t) - { t->setVisitingHero(h); - } } void GiveHero::applyGs(CGameState * gs) const @@ -2205,31 +2188,31 @@ void BattleResultAccepted::applyGs(CGameState * gs) const for(auto & res : heroResult) { if(res.hero) - res.hero->removeBonusesRecursive(Bonus::OneBattle); - } - - if(winnerSide != 2) - { - // Grow up growing artifacts - const auto hero = heroResult[winnerSide].hero; - - if (hero) - { - if(hero->commander && hero->commander->alive) - { - for(auto & art : hero->commander->artifactsWorn) - art.second.artifact->growingUp(); - } - for(auto & art : hero->artifactsWorn) - { - art.second.artifact->growingUp(); - } - } - } - - if(VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - { - if(heroResult[0].army) + res.hero->removeBonusesRecursive(Bonus::OneBattle); + } + + if(winnerSide != 2) + { + // Grow up growing artifacts + const auto hero = heroResult[winnerSide].hero; + + if (hero) + { + if(hero->commander && hero->commander->alive) + { + for(auto & art : hero->commander->artifactsWorn) + art.second.artifact->growingUp(); + } + for(auto & art : hero->artifactsWorn) + { + art.second.artifact->growingUp(); + } + } + } + + if(VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + { + if(heroResult[0].army) heroResult[0].army->giveStackExp(heroResult[0].exp); if(heroResult[1].army) heroResult[1].army->giveStackExp(heroResult[1].exp); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 0e4a384a9..36ac4e7ea 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -255,6 +255,7 @@ CGHeroInstance::CGHeroInstance(): setNodeType(HERO); ID = Obj::HERO; secSkills.emplace_back(SecondarySkill::DEFAULT, -1); + blockVisit = true; } PlayerColor CGHeroInstance::getOwner() const @@ -364,8 +365,19 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) commander->giveStackExp (exp); //after our exp is set } - if (mana < 0) - mana = manaLimit(); + skillsInfo.rand.setSeed(rand.nextInt()); + skillsInfo.resetMagicSchoolCounter(); + skillsInfo.resetWisdomCounter(); + + //copy active (probably growing) bonuses from hero prototype to hero object + for(const std::shared_ptr & b : type->specialty) + addNewBonus(b); + + //initialize bonuses + recreateSecondarySkillsBonuses(); + + movement = movementPointsLimit(true); + mana = manaLimit(); //after all bonuses are taken into account, make sure this line is the last one } void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) @@ -533,15 +545,9 @@ void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter() void CGHeroInstance::initObj(CRandomGenerator & rand) { - blockVisit = true; - if(!type) initHero(rand); //TODO: set up everything for prison before specialties are configured - skillsInfo.rand.setSeed(rand.nextInt()); - skillsInfo.resetMagicSchoolCounter(); - skillsInfo.resetWisdomCounter(); - if (ID != Obj::PRISON) { auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); @@ -549,15 +555,6 @@ void CGHeroInstance::initObj(CRandomGenerator & rand) if (customApp) appearance = customApp; } - - //copy active (probably growing) bonuses from hero prototype to hero object - for(const std::shared_ptr & b : type->specialty) - addNewBonus(b); - - //initialize bonuses - recreateSecondarySkillsBonuses(); - - mana = manaLimit(); //after all bonuses are taken into account, make sure this line is the last one } void CGHeroInstance::recreateSecondarySkillsBonuses() From 01d588ab54a50ee95a21aac93814d5bc358e3e77 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 22 Jul 2023 22:29:05 +0300 Subject: [PATCH 015/168] Fix keyboard shortcuts on selection dialogs --- client/windows/InfoWindows.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 412cced59..4b9a541bb 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -90,6 +90,15 @@ CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperl //buttons.back()->addCallback(std::bind(&CCallback::selectionMade, LOCPLINT->cb.get(), 0, askID)); } + if(buttons.size() == 1) + buttons.front()->assignedKey = EShortcut::GLOBAL_RETURN; + + if(buttons.size() == 2) + { + buttons.front()->assignedKey = EShortcut::GLOBAL_ACCEPT; + buttons.back()->assignedKey = EShortcut::GLOBAL_CANCEL; + } + for(int i=0;irecActions = 255-DISPOSE; From 5b812649ee9ca6b0b06f10e54f9837d05db508c8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 22 Jul 2023 22:45:39 +0300 Subject: [PATCH 016/168] Fix handling of "keepDisabled" option in Launcher --- launcher/modManager/cmodlist.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index 996ed5a74..f04c485e2 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -280,7 +280,7 @@ CModEntry CModList::getMod(QString modname) const if(conf.isNull()) { - settings["active"] = true; // default + settings["active"] = !local.value("keepDisabled").toBool(); } else { @@ -288,7 +288,7 @@ CModEntry CModList::getMod(QString modname) const { settings = conf.toMap(); if(settings.value("active").isNull()) - settings["active"] = true; // default + settings["active"] = !local.value("keepDisabled").toBool(); } else settings.insert("active", conf); From 5be4f6ec2f123e55a76d1484f19b82715ab93f57 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 22 Jul 2023 22:49:48 +0300 Subject: [PATCH 017/168] Fix visibility of creature amount label after casting spell before turn --- client/battle/BattleStacksController.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 55d416af3..4321d3dac 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -235,6 +235,9 @@ void BattleStacksController::setActiveStack(const CStack *stack) stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); owner.windowObject->blockUI(activeStack == nullptr); + + if (activeStack) + stackAmountBoxHidden.clear(); } bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const From 47b6358e6e463e31a2ddd8da6db10392729fd01b Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sat, 22 Jul 2023 21:51:14 +0200 Subject: [PATCH 018/168] Improve feature by triggering hero window refresh in response to netpack --- client/CPlayerInterface.cpp | 3 +++ client/battle/BattleInterface.cpp | 8 -------- client/battle/BattleWindow.cpp | 15 +++++++++++++++ client/battle/BattleWindow.h | 4 ++++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 445359148..3b70f4833 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -476,6 +476,9 @@ void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero) adventureInt->onHeroChanged(hero); if (makingTurn && hero->tempOwner == playerID) adventureInt->onHeroChanged(hero); + + for (auto window : GH.windows().findWindows()) + window->heroManaPointsChanged(hero); } void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero) { diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index c7e5cb07c..53fb5f3c7 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -584,15 +584,7 @@ void BattleInterface::endAction(const BattleAction* action) //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed if(action->actionType == EActionType::HERO_SPELL) - { fieldController->redrawBackgroundWithHexes(); - - //update casting hero info window - auto hero = action->side == 0 ? attackingHero : defendingHero; - InfoAboutHero heroInfo = InfoAboutHero(); - heroInfo.initFromHero(hero->instance(), InfoAboutHero::INBATTLE); - windowObject->updateHeroInfoWindow(action->side, heroInfo); - } } void BattleInterface::appendBattleLog(const std::string & newEntry) diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 1d0e314d1..240106a31 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -256,6 +256,21 @@ void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero panelToUpdate->update(hero); } +void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero) +{ + if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance) + { + InfoAboutHero heroInfo = InfoAboutHero(); + heroInfo.initFromHero(hero, InfoAboutHero::INBATTLE); + + updateHeroInfoWindow(hero == owner.attackingHeroInstance ? 0 : 1, heroInfo); + } + else + { + logGlobal->error("BattleWindow::heroManaPointsChanged: 'Mana points changed' called for hero not belonging to current battle window"); + } +} + void BattleWindow::activate() { GH.setStatusbar(console); diff --git a/client/battle/BattleWindow.h b/client/battle/BattleWindow.h index a16d913ca..6241e3f57 100644 --- a/client/battle/BattleWindow.h +++ b/client/battle/BattleWindow.h @@ -79,9 +79,13 @@ public: void hideQueue(); void showQueue(); + /// Toggle permanent hero info windows visibility (HD mod feature) void hideStickyHeroWindows(); void showStickyHeroWindows(); + /// Event handler for netpack changing hero mana points + void heroManaPointsChanged(const CGHeroInstance * hero); + /// block all UI elements when player is not allowed to act, e.g. during enemy turn void blockUI(bool on); From 112a40a2544207ceafa2698f76072e5519924c58 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 23 Jul 2023 11:46:11 +0300 Subject: [PATCH 019/168] Fix pinch gesture on mobile? --- client/eventsSDL/InputSourceTouch.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index 5d0827dd9..02e89015c 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -135,6 +135,12 @@ void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinge break; } case TouchState::TAP_DOWN_SHORT: + { + GH.input().setCursorPosition(convertTouchToMouse(tfinger)); + GH.events().dispatchGesturePanningStarted(lastTapPosition); + state = TouchState::TAP_DOWN_DOUBLE; + break; + } case TouchState::TAP_DOWN_PANNING: { GH.input().setCursorPosition(convertTouchToMouse(tfinger)); From 639880a42632510bc5cd169d18810e47684df054 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 23 Jul 2023 13:55:00 +0300 Subject: [PATCH 020/168] Fix handling of tactics by AI --- AI/BattleAI/BattleAI.cpp | 2 +- AI/EmptyAI/CEmptyAI.cpp | 2 +- AI/StupidAI/StupidAI.cpp | 2 +- client/Client.cpp | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index fee34710f..8c4fb4178 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -251,7 +251,7 @@ BattleAction CBattleAI::selectStackAction(const CStack * stack) void CBattleAI::yourTacticPhase(int distance) { - cb->battleMakeUnitAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); + cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); } void CBattleAI::activeStack( const CStack * stack ) diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index da206ba4b..60c13382c 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -41,7 +41,7 @@ void CEmptyAI::activeStack(const CStack * stack) void CEmptyAI::yourTacticPhase(int distance) { - cb->battleMakeUnitAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); + cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); } void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index fd69a38b2..1938b0fc3 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -90,7 +90,7 @@ static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const Battl void CStupidAI::yourTacticPhase(int distance) { - cb->battleMakeUnitAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); + cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); } void CStupidAI::activeStack( const CStack * stack ) diff --git a/client/Client.cpp b/client/Client.cpp index bed6fe7aa..5cee41b88 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -26,6 +26,7 @@ #include "../lib/gameState/CGameState.h" #include "../lib/CThreadHelper.h" #include "../lib/VCMIDirs.h" +#include "../lib/UnlockGuard.h" #include "../lib/battle/BattleInfo.h" #include "../lib/serializer/BinaryDeserializer.h" #include "../lib/mapping/CMapService.h" @@ -624,6 +625,14 @@ void CClient::battleStarted(const BattleInfo * info) CPlayerInterface::battleInt = std::make_shared(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); } } + + if(info->tacticDistance) + { + auto tacticianColor = info->sides[info->tacticsSide].color; + + if (vstd::contains(battleints, tacticianColor)) + battleints[tacticianColor]->yourTacticPhase(info->tacticDistance); + } } void CClient::battleFinished() @@ -645,6 +654,9 @@ void CClient::startPlayerBattleAction(PlayerColor color) if(vstd::contains(battleints, color)) { + // we want to avoid locking gamestate and causing UI to freeze while AI is making turn + auto unlock = vstd::makeUnlockGuardIf(*CPlayerInterface::pim, !battleints[color]->human); + assert(vstd::contains(battleints, color)); battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false)); } From ce00069dcd1d6b8a134f90dfc105e0273c1fb60e Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 23 Jul 2023 15:17:30 +0200 Subject: [PATCH 021/168] haptic feedback on radial menu action --- client/eventsSDL/InputHandler.cpp | 5 +++++ client/eventsSDL/InputHandler.h | 3 +++ client/eventsSDL/InputSourceTouch.h | 4 ++-- client/widgets/RadialMenu.cpp | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 3d46c8ef2..048112c27 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -239,6 +239,11 @@ void InputHandler::stopTextInput() textHandler->stopTextInput(); } +void InputHandler::hapticFeedback() +{ + fingerHandler->hapticFeedback(); +} + bool InputHandler::hasTouchInputDevice() const { return fingerHandler->hasTouchInputDevice(); diff --git a/client/eventsSDL/InputHandler.h b/client/eventsSDL/InputHandler.h index c620b3676..56216a6b3 100644 --- a/client/eventsSDL/InputHandler.h +++ b/client/eventsSDL/InputHandler.h @@ -63,6 +63,9 @@ public: /// Ends any existing text input state void stopTextInput(); + /// do a haptic feedback + void hapticFeedback(); + /// returns true if system has active touchscreen bool hasTouchInputDevice() const; diff --git a/client/eventsSDL/InputSourceTouch.h b/client/eventsSDL/InputSourceTouch.h index fb7277046..b82ae1688 100644 --- a/client/eventsSDL/InputSourceTouch.h +++ b/client/eventsSDL/InputSourceTouch.h @@ -96,8 +96,6 @@ class InputSourceTouch void emitPanningEvent(const SDL_TouchFingerEvent & tfinger); void emitPinchEvent(const SDL_TouchFingerEvent & tfinger); - - void hapticFeedback(); public: InputSourceTouch(); @@ -106,6 +104,8 @@ public: void handleEventFingerDown(const SDL_TouchFingerEvent & current); void handleEventFingerUp(const SDL_TouchFingerEvent & current); + void hapticFeedback(); + void handleUpdate(); bool hasTouchInputDevice() const; diff --git a/client/widgets/RadialMenu.cpp b/client/widgets/RadialMenu.cpp index 82a6a1983..2d6aa478b 100644 --- a/client/widgets/RadialMenu.cpp +++ b/client/widgets/RadialMenu.cpp @@ -13,6 +13,7 @@ #include "Images.h" #include "TextControls.h" +#include "../eventsSDL/InputHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../render/IImage.h" @@ -137,5 +138,8 @@ void RadialMenu::gesture(bool on, const Point & initialPosition, const Point & f // we need to close this window first so if action spawns a new window it won't be closed instead GH.windows().popWindows(1); if (item) + { + GH.input().hapticFeedback(); item->callback(); + } } From 1ebbb21eeacca123091a86d0b08785e919a00613 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 23 Jul 2023 15:36:53 +0200 Subject: [PATCH 022/168] Update german.json --- Mods/vcmi/config/vcmi/german.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index aab2710e3..df7a503ab 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -29,6 +29,13 @@ "vcmi.capitalColors.5" : "Violett", "vcmi.capitalColors.6" : "Türkis", "vcmi.capitalColors.7" : "Rosa", + + "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", + "vcmi.radialWheel.showUnitInformation" : "Informationen zur Kreatur anzeigen", + "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", + "vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen", + "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", + "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", "vcmi.mainMenu.tutorialNotImplemented" : "Das Tutorial ist aktuell noch nicht implementiert\n", "vcmi.mainMenu.highscoresNotImplemented" : "Die Highscores sind aktuell noch nicht implementiert\n", @@ -38,6 +45,9 @@ "vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel", "vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei", "vcmi.mainMenu.playerName" : "Spieler", + + "vcmi.lobby.filename" : "Dateiname", + "vcmi.lobby.creationDate" : "Erstellungsdatum", "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.modsIncompatibility" : "Erforderliche Mods um das Spiel zu laden:", @@ -87,6 +97,10 @@ "vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.", "vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand", "vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", @@ -113,10 +127,12 @@ "vcmi.battleOptions.movementHighlightOnHover.help": "{Hervorhebung der Bewegung bei Hover}\n\nHebt die Bewegungsreichweite der Einheit hervor, wenn man mit dem Mauszeiger über sie fährt.", "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen", "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Statistikfenster für Helden anzeigen", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.", - "vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen", "vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).", "vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).", From d1f50033c62df8212c9d66e6797515e85f06b967 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 23 Jul 2023 15:56:12 +0200 Subject: [PATCH 023/168] German update Lanucher & Mapeditor --- launcher/translation/german.ts | 8 +-- mapeditor/translation/german.ts | 96 ++++++++++++++++----------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index c667db77d..6dd2341cb 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -676,13 +676,13 @@ Before you can start playing, there are a few more steps that need to be complet Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death. Heroes® of Might and Magic® III HD is currently not supported! - Vielen Dank für die Installation von VCMI. + Vielen Dank für die Installation von VCMI! Es sind noch ein paar Schritte notwendig, bevor Sie mit dem Spielen beginnen können. Denken Sie daran, dass Sie die Originaldateien, Heroes III: Complete Edition oder Shadow of Death besitzen müssen, um VCMI verwenden zu können. -Heroes III: HD Edition wird derzeit nicht unterstützt +Heroes III: HD Edition wird derzeit nicht unterstützt! @@ -717,7 +717,7 @@ Heroes III: HD Edition wird derzeit nicht unterstützt Interface Improvements - + Interface-Verbesserungen @@ -732,7 +732,7 @@ Heroes III: HD Edition wird derzeit nicht unterstützt Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - + Installiere Mod, die verschiedene Interface-Verbesserungen bietet, wie z.B. ein besseres Interface für zufällige Karten und wählbare Aktionen in Kämpfen Install support for playing Heroes III in resolutions higher than 800x600 diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index db9d54f7b..bcc5cf31b 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -62,17 +62,17 @@ Toolbar - + Werkzeugleiste Minimap - + Minikarte Map Objects View - + Kartenobjekte-Ansicht @@ -97,7 +97,7 @@ Terrains View - + Terrain-Ansicht @@ -137,7 +137,7 @@ Save as... - Speichern unter + Speichern unter... @@ -284,7 +284,7 @@ Export as... - + Exportieren als... @@ -344,53 +344,53 @@ Difficulty - + Schwierigkeit Events - + Ereignisse Victory - + Sieg Victory message - + Sieg-Nachricht Only for human players - + Nur für menschliche Spieler Allow standard victory - + Standardsieg zulassen Parameters - + Parameter Loss - + Niederlage 7 days without town - + 7 Tage ohne Stadt Defeat message - + Niederlage-Nachricht @@ -420,67 +420,67 @@ No special victory - + Kein besonderer Sieg Capture artifact - + Artefakt sammeln Hire creatures - + Kreaturen anheuern Accumulate resources - + Ressourcen ansammeln Construct building - + Gebäude errichten Capture town - + Stadt einnehmen Defeat hero - + Held besiegen Transport artifact - + Artefakt transportieren No special loss - + Keine besondere Niederlage Lose castle - + Schloss verlieren Lose hero - + Held verlieren Time expired - + Zeit abgelaufen Days without town - + Tage ohne Stadt @@ -606,32 +606,32 @@ Map is not loaded - + Karte ist nicht geladen No players allowed to play this map - + Keine Spieler dürfen diese Karte spielen Map is allowed for one player and cannot be started - + Karte ist für einen Spieler erlaubt und kann nicht gestartet werden No human players allowed to play this map - + Keine menschlichen Spieler dürfen diese Karte spielen Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Gepanzerte Instanz %1 ist UNFLAGGABLE, muss aber NEUTRAL oder Spielerbesitzer haben Object %1 is assigned to non-playable player %2 - + Objekt %1 ist dem nicht spielbaren Spieler %2 zugewiesen @@ -641,67 +641,67 @@ Prison %1 must be a NEUTRAL - + Gefängnis %1 muss NEUTRAL sein Hero %1 must have an owner - + Held %1 muss einen Besitzer haben Hero %1 is prohibited by map settings - + Held %1 ist durch Karteneinstellungen verboten Hero %1 has duplicate on map - + Held %1 hat Duplikat auf Karte Hero %1 has an empty type and must be removed - + Held %1 hat einen leeren Typ und muss entfernt werden Spell scroll %1 is prohibited by map settings - + Zauberschriftrolle %1 ist durch Karteneinstellungen verboten Spell scroll %1 doesn't have instance assigned and must be removed - + Zauberschriftrolle %1 hat keine Instanz zugewiesen und muss entfernt werden Artifact %1 is prohibited by map settings - + Artefakt %1 ist durch Karteneinstellungen verboten Player %1 doesn't have any starting town - + Spieler %1 hat keine Startstadt Map name is not specified - + Kartenname ist nicht angegeben Map description is not specified - + Kartenbeschreibung ist nicht angegeben Exception occurs during validation: %1 - + Bei der Validierung ist eine Ausnahme aufgetreten: %1 Unknown exception occurs during validation - + Unbekannte Ausnahme trat während der Validierung auf From c40c1bf1b0481872479c8508a42fcaffc9631294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 24 Jul 2023 08:47:37 +0200 Subject: [PATCH 024/168] Added translation for the most common languages --- Mods/vcmi/config/vcmi/chinese.json | 2 +- Mods/vcmi/config/vcmi/german.json | 2 +- Mods/vcmi/config/vcmi/ukrainian.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index f86533d85..c9da783e5 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -145,7 +145,7 @@ "vcmi.questLog.hideComplete.hover" : "隐藏完成任务", "vcmi.questLog.hideComplete.help" : "隐藏所有完成的任务", - "vcmi.randomMapTab.widgets.defaultTemplate" : "默认", + "vcmi.randomMapTab.widgets.defaultTemplate" : "(随机)", "vcmi.randomMapTab.widgets.templateLabel" : "模板", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设定...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index aab2710e3..e14f2c1f1 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -170,7 +170,7 @@ "vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests", "vcmi.questLog.hideComplete.help" : "Verstecke alle Quests die bereits abgeschlossen sind", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(Standard)", + "vcmi.randomMapTab.widgets.defaultTemplate" : "(Zufällig)", "vcmi.randomMapTab.widgets.templateLabel" : "Template", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 31915f101..a04cb8092 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -153,7 +153,7 @@ "vcmi.questLog.hideComplete.hover" : "Приховати завершені квести", "vcmi.questLog.hideComplete.help" : "Приховує всі квести, які вже мають стан виконаних", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(за замовчуванням)", + "vcmi.randomMapTab.widgets.defaultTemplate" : "(Випадковий)", "vcmi.randomMapTab.widgets.templateLabel" : "Шаблон", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Налаштувати...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Розподіл команд", From 9eda30f16233c958ea2986fe0e8fda39075858c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 24 Jul 2023 16:20:01 +0200 Subject: [PATCH 025/168] Update Russian translation --- Mods/vcmi/config/vcmi/russian.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index ccafeb9d2..54ab0ca3a 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -119,7 +119,7 @@ "vcmi.questLog.hideComplete.hover" : "Скрыть завершенное", "vcmi.questLog.hideComplete.help" : "Скрыть все завершенные квесты", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(по умолчанию)", + "vcmi.randomMapTab.widgets.defaultTemplate" : "(Случайный)", "vcmi.randomMapTab.widgets.templateLabel" : "Шаблон", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Настройка...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Распределение команд", From d26e46ddfe29da5a6ae2e323f62b06131f559c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 24 Jul 2023 17:39:32 +0200 Subject: [PATCH 026/168] Remove old id, replace with randomTemplate --- Mods/vcmi/config/vcmi/chinese.json | 2 +- Mods/vcmi/config/vcmi/english.json | 2 +- Mods/vcmi/config/vcmi/french.json | 1 - Mods/vcmi/config/vcmi/german.json | 2 +- Mods/vcmi/config/vcmi/polish.json | 2 +- Mods/vcmi/config/vcmi/russian.json | 2 +- Mods/vcmi/config/vcmi/spanish.json | 1 - Mods/vcmi/config/vcmi/ukrainian.json | 2 +- client/lobby/RandomMapTab.cpp | 6 +++--- 9 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index c9da783e5..666b2168b 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -145,7 +145,7 @@ "vcmi.questLog.hideComplete.hover" : "隐藏完成任务", "vcmi.questLog.hideComplete.help" : "隐藏所有完成的任务", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(随机)", + "vcmi.randomMapTab.widgets.randomTemplate" : "(随机)", "vcmi.randomMapTab.widgets.templateLabel" : "模板", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设定...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index f7d9be64c..a32729516 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -179,7 +179,7 @@ "vcmi.questLog.hideComplete.hover" : "Hide complete quests", "vcmi.questLog.hideComplete.help" : "Hide all completed quests", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(Random)", + "vcmi.randomMapTab.widgets.randomTemplate" : "(Random)", "vcmi.randomMapTab.widgets.templateLabel" : "Template", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", diff --git a/Mods/vcmi/config/vcmi/french.json b/Mods/vcmi/config/vcmi/french.json index 5e54f9bda..1597ab88d 100644 --- a/Mods/vcmi/config/vcmi/french.json +++ b/Mods/vcmi/config/vcmi/french.json @@ -168,7 +168,6 @@ "vcmi.questLog.hideComplete.hover" : "Masquer les quêtes terminées", "vcmi.questLog.hideComplete.help" : "Masquer toutes les quêtes terminées", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(par défaut)", "vcmi.randomMapTab.widgets.templateLabel" : "Modèle", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configuration...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alignements d'équipe", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index e14f2c1f1..7636a2f09 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -170,7 +170,7 @@ "vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests", "vcmi.questLog.hideComplete.help" : "Verstecke alle Quests die bereits abgeschlossen sind", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(Zufällig)", + "vcmi.randomMapTab.widgets.randomTemplate" : "(Zufällig)", "vcmi.randomMapTab.widgets.templateLabel" : "Template", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index f41383471..9abf585bb 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -168,7 +168,7 @@ "vcmi.questLog.hideComplete.hover" : "Ukryj ukończone misje", "vcmi.questLog.hideComplete.help" : "Ukrywa wszystkie misje, które zostały zakończone", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(Losowy)", + "vcmi.randomMapTab.widgets.randomTemplate" : "(Losowy)", "vcmi.randomMapTab.widgets.templateLabel" : "Szablon", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Ustaw...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Sojusze", diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 54ab0ca3a..51d13ac0d 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -119,7 +119,7 @@ "vcmi.questLog.hideComplete.hover" : "Скрыть завершенное", "vcmi.questLog.hideComplete.help" : "Скрыть все завершенные квесты", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(Случайный)", + "vcmi.randomMapTab.widgets.randomTemplate" : "(Случайный)", "vcmi.randomMapTab.widgets.templateLabel" : "Шаблон", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Настройка...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Распределение команд", diff --git a/Mods/vcmi/config/vcmi/spanish.json b/Mods/vcmi/config/vcmi/spanish.json index 699654a0e..ffc43609b 100644 --- a/Mods/vcmi/config/vcmi/spanish.json +++ b/Mods/vcmi/config/vcmi/spanish.json @@ -142,7 +142,6 @@ "vcmi.questLog.hideComplete.hover" : "Ocultar misiones completas", "vcmi.questLog.hideComplete.help" : "Ocultar todas las misiones que ya han sido completadas", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(predeterminado)", "vcmi.randomMapTab.widgets.templateLabel" : "Plantilla", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configurar...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alineaciones de equipos", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index a04cb8092..f7a5f545e 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -153,7 +153,7 @@ "vcmi.questLog.hideComplete.hover" : "Приховати завершені квести", "vcmi.questLog.hideComplete.help" : "Приховує всі квести, які вже мають стан виконаних", - "vcmi.randomMapTab.widgets.defaultTemplate" : "(Випадковий)", + "vcmi.randomMapTab.widgets.randomTemplate" : "(Випадковий)", "vcmi.randomMapTab.widgets.templateLabel" : "Шаблон", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Налаштувати...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Розподіл команд", diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index d2f55f820..dfb0157cb 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -306,7 +306,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) if(tmpl) w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL); else - w->addTextOverlay(readText(variables["defaultTemplate"]), EFonts::FONT_SMALL); + w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL); } for(auto r : VLC->roadTypeHandler->objects) { @@ -326,7 +326,7 @@ void RandomMapTab::setTemplate(const CRmgTemplate * tmpl) if(tmpl) w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL); else - w->addTextOverlay(readText(variables["defaultTemplate"]), EFonts::FONT_SMALL); + w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL); } updateMapInfoByHost(); } @@ -387,7 +387,7 @@ void TemplatesDropBox::ListItem::updateItem(int idx, const CRmgTemplate * _item) if(idx) w->setText(""); else - w->setText(readText(dropBox.variables["defaultTemplate"])); + w->setText(readText(dropBox.variables["randomTemplate"])); } } } From 21e794268bcf67afb926cdcd5eca90ef7900bd74 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 18:24:23 +0300 Subject: [PATCH 027/168] Attempt to fix & improve error detection: - use std::fstream instead of boost::iostreams for properly working flush - removed some catch-all blocks - reduce catch scope of some try/catch blocks to clearly indicate intent --- Global.h | 20 ----- client/CServerHandler.cpp | 7 +- lib/VCMI_Lib.cpp | 15 +--- lib/gameState/CGameState.cpp | 1 - lib/logging/CLogger.h | 2 +- .../AObjectTypeHandler.cpp | 11 +-- server/CGameHandler.cpp | 4 - server/CVCMIServer.cpp | 73 +++++++------------ server/PlayerMessageProcessor.cpp | 10 +-- 9 files changed, 44 insertions(+), 99 deletions(-) diff --git a/Global.h b/Global.h index 115ae79c5..dda61c804 100644 --- a/Global.h +++ b/Global.h @@ -253,26 +253,6 @@ using TLockGuardRec = std::lock_guard; VCMI_LIB_NAMESPACE_BEGIN -void inline handleException() -{ - try - { - throw; - } - catch(const std::exception & ex) - { - logGlobal->error(ex.what()); - } - catch(const std::string & ex) - { - logGlobal->error(ex); - } - catch(...) - { - logGlobal->error("Sorry, caught unknown exception type. No more info available."); - } -} - namespace vstd { // combine hashes. Present in boost but not in std diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index a9f9b6a13..225c09c9e 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -865,8 +865,11 @@ void CServerHandler::threadHandleConnection() } else { - logNetwork->error("Lost connection to server, ending listening thread!"); - logNetwork->error(e.what()); + if (e.code() == boost::asio::error::eof) + logNetwork->error("Lost connection to server, ending listening thread! Connection has been closed"); + else + logNetwork->error("Lost connection to server, ending listening thread! Reason: %s", e.what()); + if(client) { state = EClientState::DISCONNECTING; diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 01a8c18d1..7199916a5 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -48,17 +48,10 @@ DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool { console = Console; VLC = new LibClasses(); - try - { - VLC->loadFilesystem(extractArchives); - settings.init(); - VLC->loadModFilesystem(onlyEssential); - } - catch(...) - { - handleException(); - throw; - } + VLC->loadFilesystem(extractArchives); + settings.init(); + VLC->loadModFilesystem(onlyEssential); + } DLL_LINKAGE void loadDLLClasses(bool onlyEssential) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 7108232d9..d0d62dec5 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -569,7 +569,6 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan catch(...) { logGlobal->error("Saving random map failed with exception"); - handleException(); } } diff --git a/lib/logging/CLogger.h b/lib/logging/CLogger.h index 064910383..03a36a73c 100644 --- a/lib/logging/CLogger.h +++ b/lib/logging/CLogger.h @@ -220,7 +220,7 @@ public: void write(const LogRecord & record) override; private: - FileStream file; + boost::filesystem::fstream file; CLogFormatter formatter; mutable std::mutex mx; }; diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index 1c93a6fd7..a7c672533 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -77,15 +77,8 @@ void AObjectTypeHandler::init(const JsonNode & input) tmpl->id = Obj(type); tmpl->subid = subtype; tmpl->stringID = entry.first; // FIXME: create "fullID" - type.object.template? - try - { - tmpl->readJson(entry.second); - templates.push_back(std::shared_ptr(tmpl)); - } - catch (const std::exception & e) - { - logGlobal->warn("Failed to load terrains for object %s: %s", entry.first, e.what()); - } + tmpl->readJson(entry.second); + templates.push_back(std::shared_ptr(tmpl)); } for(const JsonNode & node : input["sounds"]["ambient"].Vector()) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 41ed3caa1..ac196ab56 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -109,10 +109,6 @@ public: (void)e; return false; } - catch(...) - { - throw; - } } }; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index d8ce1fc34..8a9ad301e 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -409,51 +409,36 @@ void CVCMIServer::threadHandleClient(std::shared_ptr c) setThreadName("CVCMIServer::handleConnection"); c->enterLobbyConnectionMode(); -#ifndef _MSC_VER - try + while(c->connected) { -#endif - while(c->connected) + CPack * pack; + + try { - CPack * pack; - - try - { - pack = c->retrievePack(); - } - catch(boost::system::system_error & e) - { - logNetwork->error("Network error receiving a pack. Connection %s dies. What happened: %s", c->toString(), e.what()); - hangingConnections.insert(c); - connections.erase(c); - if(connections.empty() || hostClient == c) - state = EServerState::SHUTDOWN; - - if(gh && state == EServerState::GAMEPLAY) - { - gh->handleClientDisconnection(c); - } - break; - } - - CVCMIServerPackVisitor visitor(*this, this->gh); - pack->visit(visitor); + pack = c->retrievePack(); } -#ifndef _MSC_VER + catch(boost::system::system_error & e) + { + if (e.code() == boost::asio::error::eof) + logNetwork->error("Network error receiving a pack. Connection has been closed"); + else + logNetwork->error("Network error receiving a pack. Connection %s dies. What happened: %s", c->toString(), e.what()); + + hangingConnections.insert(c); + connections.erase(c); + if(connections.empty() || hostClient == c) + state = EServerState::SHUTDOWN; + + if(gh && state == EServerState::GAMEPLAY) + { + gh->handleClientDisconnection(c); + } + break; + } + + CVCMIServerPackVisitor visitor(*this, this->gh); + pack->visit(visitor); } - catch(const std::exception & e) - { - (void)e; - boost::unique_lock queueLock(mx); - logNetwork->error("%s dies... \nWhat happened: %s", c->toString(), e.what()); - } - catch(...) - { - state = EServerState::SHUTDOWN; - handleException(); - throw; - } -#endif boost::unique_lock queueLock(mx); @@ -1022,7 +1007,7 @@ static void handleCommandOptions(int argc, const char * argv[], boost::program_o { po::store(po::parse_command_line(argc, argv, opts), options); } - catch(std::exception & e) + catch(po::error & e) { std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; } @@ -1113,10 +1098,6 @@ int main(int argc, const char * argv[]) logNetwork->error(e.what()); server.state = EServerState::SHUTDOWN; } - catch(...) - { - handleException(); - } } catch(boost::system::system_error & e) { diff --git a/server/PlayerMessageProcessor.cpp b/server/PlayerMessageProcessor.cpp index 28713c413..268802a9a 100644 --- a/server/PlayerMessageProcessor.cpp +++ b/server/PlayerMessageProcessor.cpp @@ -175,7 +175,7 @@ void PlayerMessageProcessor::cheatGiveArmy(PlayerColor player, const CGHeroInsta { amountPerSlot = std::stol(words.at(1)); } - catch(std::exception&) + catch(std::logic_error&) { } @@ -233,7 +233,7 @@ void PlayerMessageProcessor::cheatLevelup(PlayerColor player, const CGHeroInstan { levelsToGain = std::stol(words.at(0)); } - catch(std::exception&) + catch(std::logic_error&) { levelsToGain = 1; } @@ -252,7 +252,7 @@ void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroIns { expAmountProcessed = std::stol(words.at(0)); } - catch(std::exception&) + catch(std::logic_error&) { expAmountProcessed = 10000; } @@ -271,7 +271,7 @@ void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInsta { smp.val = std::stol(words.at(0));; } - catch(std::exception&) + catch(std::logic_error&) { smp.val = 1000000; } @@ -293,7 +293,7 @@ void PlayerMessageProcessor::cheatResources(PlayerColor player, std::vector Date: Mon, 24 Jul 2023 18:24:57 +0200 Subject: [PATCH 028/168] Fix autosave at end of turn starting with AI turn on load --- client/adventureMap/AdventureMapInterface.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index d8a8d7f28..5d9c0a276 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -417,14 +417,14 @@ void AdventureMapInterface::hotkeyEndingTurn() if(settings["session"]["spectate"].Bool()) return; - LOCPLINT->makingTurn = false; - LOCPLINT->cb->endTurn(); - if(!settings["general"]["startTurnAutosave"].Bool()) { LOCPLINT->performAutosave(); } + LOCPLINT->makingTurn = false; + LOCPLINT->cb->endTurn(); + mapAudio->onPlayerTurnEnded(); } From e23166df88d31e628b9b8635141d0349485ccfaa Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 23:01:18 +0300 Subject: [PATCH 029/168] Fix locking of mutexes by StupidAI --- AI/BattleAI/BattleAI.h | 3 ++- AI/StupidAI/StupidAI.cpp | 14 +++++++++++++- AI/StupidAI/StupidAI.h | 3 +++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index de0d02c48..d9c932f1e 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -59,7 +59,8 @@ class CBattleAI : public CBattleGameInterface std::shared_ptr env; //Previous setting of cb - bool wasWaitingForRealize, wasUnlockingGs; + bool wasWaitingForRealize; + bool wasUnlockingGs; int movesSkippedByDefense; public: diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 1938b0fc3..415ce5a2c 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -18,14 +18,21 @@ static std::shared_ptr cbc; CStupidAI::CStupidAI() : side(-1) + , wasWaitingForRealize(false) + , wasUnlockingGs(false) { print("created"); } - CStupidAI::~CStupidAI() { print("destroyed"); + if(cb) + { + //Restore previous state of CB - it may be shared with the main AI (like VCAI) + cb->waitTillRealize = wasWaitingForRealize; + cb->unlockGsWhenWaiting = wasUnlockingGs; + } } void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) @@ -33,6 +40,11 @@ void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::share print("init called, saving ptr to IBattleCallback"); env = ENV; cbc = cb = CB; + + wasWaitingForRealize = CB->waitTillRealize; + wasUnlockingGs = CB->unlockGsWhenWaiting; + CB->waitTillRealize = false; + CB->unlockGsWhenWaiting = false; } void CStupidAI::actionFinished(const BattleAction &action) diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 593f1fa11..afb0f56a6 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -20,6 +20,9 @@ class CStupidAI : public CBattleGameInterface std::shared_ptr cb; std::shared_ptr env; + bool wasWaitingForRealize; + bool wasUnlockingGs; + void print(const std::string &text) const; public: CStupidAI(); From f827571f9b0db40a608ce1f50c3d5f34b4d37895 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 23:01:44 +0300 Subject: [PATCH 030/168] Fix assertion failure on exit due to mutex lock --- client/eventsSDL/InputHandler.cpp | 15 ++++++++++++--- client/eventsSDL/InputHandler.h | 2 ++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 048112c27..c65fefef0 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -70,13 +70,22 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current) } } -void InputHandler::processEvents() +std::vector InputHandler::acquireEvents() { boost::unique_lock lock(eventsMutex); - for(const auto & currentEvent : eventsQueue) + + std::vector result; + std::swap(result, eventsQueue); + return result; +} + +void InputHandler::processEvents() +{ + std::vector eventsToProcess = acquireEvents(); + + for(const auto & currentEvent : eventsToProcess) handleCurrentEvent(currentEvent); - eventsQueue.clear(); fingerHandler->handleUpdate(); } diff --git a/client/eventsSDL/InputHandler.h b/client/eventsSDL/InputHandler.h index 56216a6b3..ac245f247 100644 --- a/client/eventsSDL/InputHandler.h +++ b/client/eventsSDL/InputHandler.h @@ -29,6 +29,8 @@ class InputHandler Point cursorPosition; + std::vector acquireEvents(); + void preprocessEvent(const SDL_Event & event); void handleCurrentEvent(const SDL_Event & current); void handleUserEvent(const SDL_UserEvent & current); From 37437224f9fcaadcfc149092ee025a5ed0a833c2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 23:16:34 +0300 Subject: [PATCH 031/168] Remove excessive warning --- lib/CTownHandler.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 466031c7d..78c849e92 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -595,7 +595,8 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons if(ret->bid == BuildingID::NONE && !source["id"].isNull()) { - logMod->warn("Building %s: id field is deprecated", stringID); + // FIXME: A lot of false-positives with no clear way to handle them in mods + //logMod->warn("Building %s: id field is deprecated", stringID); ret->bid = source["id"].isNull() ? BuildingID(BuildingID::NONE) : BuildingID(source["id"].Float()); } From bf7d8ba6de1603fa3a215306aed99839ffc347aa Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 23:16:44 +0300 Subject: [PATCH 032/168] Fix typo --- Mods/vcmi/config/vcmi/ukrainian.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 8122e1df8..e4d03e423 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -85,7 +85,7 @@ "vcmi.systemOptions.framerateButton.hover" : "Лічильник кадрів", "vcmi.systemOptions.framerateButton.help" : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Тактильний відгук", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильний відгук}\n\Використовувати вібрацію при використанні сенсорного екрану", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильний відгук}\n\nВикористовувати вібрацію при використанні сенсорного екрану", "vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна", "vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу", From 823080be86cbf66e1f64f7c32eb8a69e279e2996 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 23:27:26 +0300 Subject: [PATCH 033/168] Fix error on opening map list with hota maps --- client/lobby/SelectionTab.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index a0016634e..84b430f32 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -194,11 +194,8 @@ SelectionTab::SelectionTab(ESelectionScreen Type) } iconsMapFormats = std::make_shared("SCSELC.DEF"); - iconsMapFormats->preload(); iconsVictoryCondition = std::make_shared("SCNRVICT.DEF"); - iconsVictoryCondition->preload(); iconsLossCondition = std::make_shared("SCNRLOSS.DEF"); - iconsLossCondition->preload(); for(int i = 0; i < positionsToShow; i++) listItems.push_back(std::make_shared(Point(30, 129 + i * 25), iconsMapFormats, iconsVictoryCondition, iconsLossCondition)); From fe9090ddfa8bbc06908d1037df07631652bfe0b1 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Tue, 25 Jul 2023 10:09:18 +0300 Subject: [PATCH 034/168] #2044 - changed order of destructors to avoid crash --- lib/gameState/CGameState.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 7108232d9..6e1989cf9 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -394,8 +394,8 @@ CGameState::CGameState() CGameState::~CGameState() { - map.dellNull(); curB.dellNull(); + map.dellNull(); } void CGameState::preInit(Services * services) From ec8898a0e7493327710c934626c9f0f6921d0e86 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Mon, 24 Jul 2023 22:21:15 +0300 Subject: [PATCH 035/168] #1965 - treat dwelling defenders as neutrals --- lib/battle/SideInBattle.cpp | 8 ++++++++ lib/mapObjects/IObjectInterface.cpp | 4 ++++ server/CGameHandler.cpp | 16 ++++++---------- server/CGameHandler.h | 3 ++- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/battle/SideInBattle.cpp b/lib/battle/SideInBattle.cpp index 1fd2c63d0..8dee40f1e 100644 --- a/lib/battle/SideInBattle.cpp +++ b/lib/battle/SideInBattle.cpp @@ -19,6 +19,14 @@ void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army armyObject = Army; color = armyObject->getOwner(); + if(armyObject->ID == Obj::CREATURE_GENERATOR1 + || armyObject->ID == Obj::CREATURE_GENERATOR2 + || armyObject->ID == Obj::CREATURE_GENERATOR3 + || armyObject->ID == Obj::CREATURE_GENERATOR4) + { + color = PlayerColor::NEUTRAL; + } + if(color == PlayerColor::UNFLAGGABLE) color = PlayerColor::NEUTRAL; } diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index 967470804..b76106232 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -107,6 +107,10 @@ int3 IBoatGenerator::bestLocation() const IBoatGenerator::EGeneratorState IBoatGenerator::shipyardStatus() const { int3 tile = bestLocation(); + + if(!tile.valid()) + return TILE_BLOCKED; //no available water + const TerrainTile *t = IObjectInterface::cb->getTile(tile); if(!t) return TILE_BLOCKED; //no available water diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 41ed3caa1..93685c12b 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -635,11 +635,9 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) return; } - const CArmedInstance *bEndArmy1 = battleInfo->sides.at(0).armyObject; - const CArmedInstance *bEndArmy2 = battleInfo->sides.at(1).armyObject; const BattleResult::EResult result = battleResult.get()->result; - CasualtiesAfterBattle cab1(bEndArmy1, battleInfo), cab2(bEndArmy2, battleInfo); //calculate casualties before deleting battle + CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle ChangeSpells cs; //for Eagle Eye if(!finishingBattle->isDraw() && finishingBattle->winnerHero) @@ -816,8 +814,8 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); BattleResultAccepted raccepted; - raccepted.heroResult[0].army = const_cast(bEndArmy1); - raccepted.heroResult[1].army = const_cast(bEndArmy2); + raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); + raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); raccepted.heroResult[0].exp = battleResult.data->exp[0]; @@ -6684,14 +6682,12 @@ void CGameHandler::showInfoDialog(const std::string & msg, PlayerColor player) showInfoDialog(&iw); } -CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, const BattleInfo * bat): - army(_army) +CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): + army(battleSide.armyObject) { heroWithDeadCommander = ObjectInstanceID(); - PlayerColor color = army->tempOwner; - if(color == PlayerColor::UNFLAGGABLE) - color = PlayerColor::NEUTRAL; + PlayerColor color = battleSide.color; for(CStack * st : bat->stacks) { diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 771ba90f1..a3f906408 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -23,6 +23,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGameState; struct StartInfo; struct BattleResult; +struct SideInBattle; struct BattleAttack; struct BattleStackAttacked; struct CPack; @@ -90,7 +91,7 @@ struct CasualtiesAfterBattle TSummoned summoned; ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations - CasualtiesAfterBattle(const CArmedInstance * _army, const BattleInfo * bat); + CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); void updateArmy(CGameHandler *gh); }; From b4c9b81a33620a75b9334ca39b74cebc1cc0dc3b Mon Sep 17 00:00:00 2001 From: DjWarmonger Date: Tue, 25 Jul 2023 10:04:09 +0200 Subject: [PATCH 036/168] Update SideInBattle.cpp --- lib/battle/SideInBattle.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/battle/SideInBattle.cpp b/lib/battle/SideInBattle.cpp index 8dee40f1e..aeafb243c 100644 --- a/lib/battle/SideInBattle.cpp +++ b/lib/battle/SideInBattle.cpp @@ -17,14 +17,17 @@ void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army { hero = Hero; armyObject = Army; - color = armyObject->getOwner(); - if(armyObject->ID == Obj::CREATURE_GENERATOR1 - || armyObject->ID == Obj::CREATURE_GENERATOR2 - || armyObject->ID == Obj::CREATURE_GENERATOR3 - || armyObject->ID == Obj::CREATURE_GENERATOR4) + switch(armyObject->ID) { - color = PlayerColor::NEUTRAL; + case Obj::CREATURE_GENERATOR1: + case Obj::CREATURE_GENERATOR2: + case Obj::CREATURE_GENERATOR3: + case Obj::CREATURE_GENERATOR4: + color = PlayerColor::NEUTRAL; + break; + default: + color = armyObject->getOwner(); } if(color == PlayerColor::UNFLAGGABLE) From e4303c63ce2bcd17f7a366d01f98310cf81572c8 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Tue, 25 Jul 2023 17:13:02 +0200 Subject: [PATCH 037/168] Add json entries for missing bonuses --- config/bonuses.json | 58 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/config/bonuses.json b/config/bonuses.json index 3b55de5b8..66b22babd 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -35,6 +35,14 @@ } }, + "BLOCKS_RANGED_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/RANGEDBLOCK" + } + }, + "BLOCKS_RETALIATION": { "graphics": @@ -100,6 +108,14 @@ } }, + "DESTRUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/DESTROYER" + } + }, + "DOUBLE_DAMAGE_CHANCE": { "graphics": @@ -168,7 +184,15 @@ "icon": "zvs/Lib1.res/FireShield" } }, - + + "FIRST_STRIKE": + { + "graphics": + { + "icon": "zvs/Lib1.res/FIRSTSTRIKE" + } + }, + "FEAR": { "graphics": @@ -396,6 +420,14 @@ "hidden": true }, + "RANGED_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/RANGEDCOUNTER" + } + }, + "RECEPTIVE": { "graphics": @@ -427,6 +459,14 @@ "icon": "zvs/Lib1.res/E_SHOOT" } }, + + "SHOOTS_ALL_ADJACENT": + { + "graphics": + { + "icon": "zvs/Lib1.res/AREASHOT" + } + }, "SOUL_STEAL": { @@ -492,6 +532,14 @@ } }, + "SUMMON_GUARDIANS": + { + "graphics": + { + "icon": "zvs/Lib1.res/SUMMONGUARDS" + } + }, + "TWO_HEX_ATTACK_BREATH": { "graphics": @@ -543,6 +591,14 @@ { "icon": "zvs/Lib1.res/E_SPWATER1" } + }, + + "WIDE_BREATH": + { + "graphics": + { + "icon": "zvs/Lib1.res/MEGABREATH" + } } } From 32f7a95e38576d7abe217884ab94f817834920ce Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 25 Jul 2023 18:50:55 +0300 Subject: [PATCH 038/168] Use capture by value since thread might start after local method returns --- client/battle/BattleInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 53fb5f3c7..1f49a635c 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -727,7 +727,7 @@ void BattleInterface::requestAutofightingAIToTakeAction() // FIXME: unsafe // Run task in separate thread to avoid UI lock while AI is making turn (which might take some time) // HOWEVER this thread won't atttempt to lock game state, potentially leading to races - boost::thread aiThread([&]() + boost::thread aiThread([this, activeStack]() { curInt->autofightingAI->activeStack(activeStack); }); From e733b55c90ec7c714ecc2dc7c1cb6fefb367063b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 25 Jul 2023 22:36:45 +0300 Subject: [PATCH 039/168] Removed buggy and poorly designed fromString method Use VLC->modh directly with proper parameters instead --- lib/GameConstants.cpp | 30 ------------------- lib/GameConstants.h | 5 ---- lib/gameState/CGameState.cpp | 3 +- .../AObjectTypeHandler.cpp | 16 ++++++---- .../AObjectTypeHandler.h | 2 +- server/CGameHandler.cpp | 2 +- test/game/CGameStateTest.cpp | 2 +- test/mock/BattleFake.cpp | 2 +- 8 files changed, 16 insertions(+), 46 deletions(-) diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index c440277de..5a1ffe19a 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -305,44 +305,14 @@ bool operator<(const BattleField & l, const BattleField & r) return l.num < r.num; } -BattleField::operator std::string() const -{ - return getInfo()->identifier; -} - const BattleFieldInfo * BattleField::getInfo() const { return VLC->battlefields()->getById(*this); } -BattleField BattleField::fromString(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "battlefield", identifier); - - if(rawId) - return BattleField(rawId.value()); - else - return BattleField::NONE; -} - const ObstacleInfo * Obstacle::getInfo() const { return VLC->obstacles()->getById(*this); } -Obstacle::operator std::string() const -{ - return getInfo()->identifier; -} - -Obstacle Obstacle::fromString(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "obstacle", identifier); - - if(rawId) - return Obstacle(rawId.value()); - else - return Obstacle(-1); -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/GameConstants.h b/lib/GameConstants.h index 93a3085a3..ab2aaed89 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -1292,10 +1292,7 @@ class BattleField : public BaseForID DLL_LINKAGE friend bool operator!=(const BattleField & l, const BattleField & r); DLL_LINKAGE friend bool operator<(const BattleField & l, const BattleField & r); - DLL_LINKAGE operator std::string() const; DLL_LINKAGE const BattleFieldInfo * getInfo() const; - - DLL_LINKAGE static BattleField fromString(const std::string & identifier); }; enum class EBoatId : int32_t @@ -1336,8 +1333,6 @@ class Obstacle : public BaseForID INSTID_LIKE_CLASS_COMMON(Obstacle, si32) DLL_LINKAGE const ObstacleInfo * getInfo() const; - DLL_LINKAGE operator std::string() const; - DLL_LINKAGE static Obstacle fromString(const std::string & identifier); }; enum class ESpellSchool: int8_t diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 911584a30..4d6e572d4 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1244,12 +1244,11 @@ BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & r } if(map->isCoastalTile(tile)) //coastal tile is always ground - return BattleField::fromString("sand_shore"); + return BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield", "sand_shore")); return BattleField(*RandomGeneratorUtil::nextItem(t.terType->battleFields, rand)); } - void CGameState::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const { assert(obj); diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index a7c672533..53a19c83b 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -13,6 +13,7 @@ #include "IObjectInfo.h" #include "../CGeneralTextHandler.h" +#include "../CModHandler.h" #include "../VCMI_Lib.h" #include "../mapObjects/CGObjectInstance.h" #include "../mapObjects/ObjectTemplate.h" @@ -95,10 +96,15 @@ void AObjectTypeHandler::init(const JsonNode & input) else aiValue = static_cast>(input["aiValue"].Integer()); - if(input["battleground"].getType() == JsonNode::JsonType::DATA_STRING) - battlefield = input["battleground"].String(); - else - battlefield = std::nullopt; + battlefield = BattleField::NONE; + + if(!input["battleground"].isNull()) + { + VLC->modh->identifiers.requestIdentifier("battlefield", input["battleground"], [this](int32_t identifier) + { + battlefield = BattleField(identifier); + }); + } initTypeData(input); } @@ -175,7 +181,7 @@ std::vector> AObjectTypeHandler::getTempla BattleField AObjectTypeHandler::getBattlefield() const { - return battlefield ? BattleField::fromString(battlefield.value()) : BattleField::NONE; + return battlefield; } std::vector>AObjectTypeHandler::getTemplates(TerrainId terrainType) const diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.h b/lib/mapObjectConstructors/AObjectTypeHandler.h index f6776b0ac..ce35a9f82 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.h +++ b/lib/mapObjectConstructors/AObjectTypeHandler.h @@ -34,7 +34,7 @@ class DLL_LINKAGE AObjectTypeHandler : public boost::noncopyable SObjectSounds sounds; std::optional aiValue; - std::optional battlefield; + BattleField battlefield; std::string modScope; std::string typeName; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index a847ad764..9df39108a 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2113,7 +2113,7 @@ void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const BattleField terType = gs->battleGetBattlefieldType(tile, getRandomGenerator()); if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) - terType = BattleField::fromString("ship_to_ship"); + terType = BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield", "ship_to_ship")); //send info about battles BattleStart bs; diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index ded6e8baa..ae9bd2af0 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -195,7 +195,7 @@ public: const auto & t = *gameCallback->getTile(tile); auto terrain = t.terType->getId(); - BattleField terType = BattleField::fromString("grass_hills"); + BattleField terType(0); //send info about battles diff --git a/test/mock/BattleFake.cpp b/test/mock/BattleFake.cpp index b1e2af5c4..a2f5a9337 100644 --- a/test/mock/BattleFake.cpp +++ b/test/mock/BattleFake.cpp @@ -94,7 +94,7 @@ void BattleFake::setupEmptyBattlefield() { EXPECT_CALL(*this, getDefendedTown()).WillRepeatedly(Return(nullptr)); EXPECT_CALL(*this, getAllObstacles()).WillRepeatedly(Return(IBattleInfo::ObstacleCList())); - EXPECT_CALL(*this, getBattlefieldType()).WillRepeatedly(Return(BattleField::fromString("grass_hills"))); + EXPECT_CALL(*this, getBattlefieldType()).WillRepeatedly(Return(BattleField(0))); } #if SCRIPTING_ENABLED From 27471748101d6e02d0065a802c67efeb16944b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Wed, 26 Jul 2023 17:12:09 +0200 Subject: [PATCH 040/168] Fix incorrect sand shore id --- lib/gameState/CGameState.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 4d6e572d4..780ed3cb2 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1244,7 +1244,7 @@ BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & r } if(map->isCoastalTile(tile)) //coastal tile is always ground - return BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield", "sand_shore")); + return BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield.sand_shore")); return BattleField(*RandomGeneratorUtil::nextItem(t.terType->battleFields, rand)); } From ff1a635e9e9fa72896d236f63b20a8c8435e0c50 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Wed, 26 Jul 2023 21:20:11 +0200 Subject: [PATCH 041/168] Unblock basic adventure map actions (scrolling / right-click) in multiplayer --- client/adventureMap/AdventureMapInterface.cpp | 2 +- client/adventureMap/AdventureMapShortcuts.cpp | 16 +++++++++++++++- client/adventureMap/AdventureMapShortcuts.h | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 5d9c0a276..fba871751 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -192,7 +192,7 @@ void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed) Point scrollDelta = scrollDirection * scrollDistance; bool cursorInScrollArea = scrollDelta != Point(0,0); - bool scrollingActive = cursorInScrollArea && isActive() && shortcuts->optionSidePanelActive() && !scrollingWasBlocked; + bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked; bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); if (!scrollingWasActive && scrollingBlocked) diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index 722814d08..1892ef448 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -36,6 +36,14 @@ #include "../../lib/mapping/CMap.h" #include "../../lib/pathfinder/CGPathNode.h" +bool isCurrentPlayerHuman() +{ + PlayerColor currentPlayer = LOCPLINT->cb->getCurrentPlayer(); + bool isHuman = LOCPLINT->cb->getStartInfo()->playerInfos.count(currentPlayer) + && LOCPLINT->cb->getStartInfo()->playerInfos.at(currentPlayer).isControlledByHuman(); + return isHuman; +} + AdventureMapShortcuts::AdventureMapShortcuts(AdventureMapInterface & owner) : owner(owner) , state(EAdventureState::NOT_INITIALIZED) @@ -461,7 +469,13 @@ bool AdventureMapShortcuts::optionSidePanelActive() return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW; } +bool AdventureMapShortcuts::optionMapScrollingActive() +{ + return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || (state == EAdventureState::ENEMY_TURN && isCurrentPlayerHuman()); +} + bool AdventureMapShortcuts::optionMapViewActive() { - return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL; + return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL + || (state == EAdventureState::ENEMY_TURN && isCurrentPlayerHuman()); } diff --git a/client/adventureMap/AdventureMapShortcuts.h b/client/adventureMap/AdventureMapShortcuts.h index 770e40fa2..8e86779a7 100644 --- a/client/adventureMap/AdventureMapShortcuts.h +++ b/client/adventureMap/AdventureMapShortcuts.h @@ -81,6 +81,7 @@ public: bool optionInMapView(); bool optionInWorldView(); bool optionSidePanelActive(); + bool optionMapScrollingActive(); bool optionMapViewActive(); void setState(EAdventureState newState); From 72210afc926a386639a115063d0ebe8a64d45400 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 27 Jul 2023 15:51:38 +0300 Subject: [PATCH 042/168] Do not attempt to reset movement for inactive heroes in pool --- lib/gameState/TavernHeroesPool.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index 895c052e2..f5e5a6138 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -109,12 +109,18 @@ CGHeroInstance * TavernHeroesPool::takeHeroFromPool(HeroTypeID hero) void TavernHeroesPool::onNewDay() { + auto unusedHeroes = unusedHeroesFromPool(); + for(auto & hero : heroesPool) { assert(hero.second); if(!hero.second) continue; + // do not access heroes who are not present in tavern of any players + if (vstd::contains(unusedHeroes, hero.first)) + continue; + hero.second->setMovementPoints(hero.second->movementPointsLimit(true)); hero.second->mana = hero.second->manaLimit(); } From 0b5cea032028caad42fb85e00cdc4e42a61b7669 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Thu, 27 Jul 2023 17:26:52 +0200 Subject: [PATCH 043/168] Use new adventure map state to detect opponent human turn --- client/adventureMap/AdventureMapInterface.cpp | 2 +- client/adventureMap/AdventureMapShortcuts.cpp | 12 ++---------- client/adventureMap/AdventureState.h | 3 ++- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index fba871751..0f7ef98e2 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -323,7 +323,7 @@ void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuma mapAudio->onEnemyTurnStarted(); widget->getMinimap()->setAIRadar(!isHuman); widget->getInfoBar()->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer()); - setState(EAdventureState::ENEMY_TURN); + setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN); } void AdventureMapInterface::setState(EAdventureState state) diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index 1892ef448..3fb44f0dc 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -36,14 +36,6 @@ #include "../../lib/mapping/CMap.h" #include "../../lib/pathfinder/CGPathNode.h" -bool isCurrentPlayerHuman() -{ - PlayerColor currentPlayer = LOCPLINT->cb->getCurrentPlayer(); - bool isHuman = LOCPLINT->cb->getStartInfo()->playerInfos.count(currentPlayer) - && LOCPLINT->cb->getStartInfo()->playerInfos.at(currentPlayer).isControlledByHuman(); - return isHuman; -} - AdventureMapShortcuts::AdventureMapShortcuts(AdventureMapInterface & owner) : owner(owner) , state(EAdventureState::NOT_INITIALIZED) @@ -471,11 +463,11 @@ bool AdventureMapShortcuts::optionSidePanelActive() bool AdventureMapShortcuts::optionMapScrollingActive() { - return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || (state == EAdventureState::ENEMY_TURN && isCurrentPlayerHuman()); + return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || (state == EAdventureState::OTHER_HUMAN_PLAYER_TURN); } bool AdventureMapShortcuts::optionMapViewActive() { return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL - || (state == EAdventureState::ENEMY_TURN && isCurrentPlayerHuman()); + || (state == EAdventureState::OTHER_HUMAN_PLAYER_TURN); } diff --git a/client/adventureMap/AdventureState.h b/client/adventureMap/AdventureState.h index 628e8f976..32bfc2e2d 100644 --- a/client/adventureMap/AdventureState.h +++ b/client/adventureMap/AdventureState.h @@ -14,7 +14,8 @@ enum class EAdventureState NOT_INITIALIZED, HOTSEAT_WAIT, MAKING_TURN, - ENEMY_TURN, + AI_PLAYER_TURN, + OTHER_HUMAN_PLAYER_TURN, CASTING_SPELL, WORLD_VIEW }; From b8110218c07acfb97380ee142ab632c27a7d961c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 27 Jul 2023 18:39:05 +0300 Subject: [PATCH 044/168] Silence unnecessary warning on flagging guarded garrison --- server/CQuery.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/CQuery.cpp b/server/CQuery.cpp index dfa0c8848..1d0a7d7b3 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -176,7 +176,7 @@ void CObjectVisitQuery::onExposure(QueryPtr topQuery) if(gh->isValidObject(visitedObject)) topQuery->notifyObjectAboutRemoval(*this); - owner->popQuery(*this); + owner->popIfTop(*this); } void Queries::popQuery(PlayerColor player, QueryPtr query) From aed8c411fc38f153ba5caf02abb4585283a4ee95 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 27 Jul 2023 18:40:47 +0300 Subject: [PATCH 045/168] Minor rework & cleanup of combat replays --- AI/BattleAI/BattleAI.cpp | 2 +- AI/BattleAI/BattleAI.h | 2 +- AI/Nullkiller/AIGateway.cpp | 22 ++++++++++-------- AI/Nullkiller/AIGateway.h | 2 +- AI/StupidAI/StupidAI.cpp | 2 +- AI/StupidAI/StupidAI.h | 2 +- AI/VCAI/VCAI.cpp | 20 ++++++++++------- AI/VCAI/VCAI.h | 2 +- client/CPlayerInterface.cpp | 34 +++++++++++++--------------- client/CPlayerInterface.h | 4 +--- client/Client.cpp | 2 +- client/battle/BattleWindow.cpp | 2 +- config/schemas/settings.json | 4 ++-- lib/CGameInterface.cpp | 4 ++-- lib/CGameInterface.h | 2 +- lib/IGameEventsReceiver.h | 2 +- lib/battle/BattleInfo.cpp | 1 + lib/battle/BattleInfo.h | 5 +++++ lib/serializer/CSerializer.h | 2 +- server/CGameHandler.cpp | 41 ++++++++++++++++++++++------------ 20 files changed, 90 insertions(+), 67 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 8c4fb4178..d3c4ba13c 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -826,7 +826,7 @@ void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcas ps.value = totalGain; } -void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) +void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) { LOG_TRACE(logAi); side = Side; diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index d9c932f1e..37338d299 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -83,7 +83,7 @@ public: BattleAction selectStackAction(const CStack * stack); std::optional findBestCreatureSpell(const CStack *stack); - void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side) override; + void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override; //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 091dbe407..16cd34849 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -809,7 +809,7 @@ void AIGateway::makeTurn() for (auto h : cb->getHeroesInfo()) { if (h->movementPointsRemaining()) - logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); + logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); } #if NKAI_TRACE_LEVEL == 0 } @@ -1065,14 +1065,14 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re } } -void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) +void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) { NET_EVENT_HANDLER; assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); status.setBattle(ONGOING_BATTLE); const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); - CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); + CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); } void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) @@ -1083,12 +1083,16 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) bool won = br->winner == myCb->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); - status.addQuery(queryID, "Combat result dialog"); - const int confirmAction = 0; - requestActionASAP([=]() + + if (queryID != -1) { - answerQuery(queryID, confirmAction); - }); + status.addQuery(queryID, "Combat result dialog"); + const int confirmAction = 0; + requestActionASAP([=]() + { + answerQuery(queryID, confirmAction); + }); + } CAdventureAI::battleEnd(br, queryID); } @@ -1175,7 +1179,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h) if(startHpos == dst) { //FIXME: this assertion fails also if AI moves onto defeated guarded object - assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object + //assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object cb->moveHero(*h, h->convertFromVisitablePos(dst)); afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly? // If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index a29197b9e..cab6ce797 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -169,7 +169,7 @@ public: void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; - void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; + void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; void battleEnd(const BattleResult * br, QueryID queryID) override; void makeTurn(); diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 415ce5a2c..85a9fe571 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -242,7 +242,7 @@ void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse) print("battleStacksEffectsSet called"); } -void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) +void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) { print("battleStart called"); side = Side; diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index afb0f56a6..3b074d643 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -44,7 +44,7 @@ public: void battleSpellCast(const BattleSpellCast *sc) override; void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleTriggerEffect(const BattleTriggerEffect & bte) override; - void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right + void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack private: diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index cdaa021ab..c3bd49b6d 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -819,7 +819,7 @@ void VCAI::makeTurn() for (auto h : cb->getHeroesInfo()) { if (h->movementPointsRemaining()) - logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); + logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); } } catch (boost::thread_interrupted & e) @@ -1575,14 +1575,14 @@ void VCAI::completeGoal(Goals::TSubgoal goal) } -void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) +void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) { NET_EVENT_HANDLER; assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); status.setBattle(ONGOING_BATTLE); const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); - CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); + CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); } void VCAI::battleEnd(const BattleResult * br, QueryID queryID) @@ -1593,12 +1593,16 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID) bool won = br->winner == myCb->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); - status.addQuery(queryID, "Combat result dialog"); - const int confirmAction = 0; - requestActionASAP([=]() + + if (queryID != -1) { - answerQuery(queryID, confirmAction); - }); + status.addQuery(queryID, "Combat result dialog"); + const int confirmAction = 0; + requestActionASAP([=]() + { + answerQuery(queryID, confirmAction); + }); + } CAdventureAI::battleEnd(br, queryID); } diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index a932d8702..7d57ad681 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -201,7 +201,7 @@ public: void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; + void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; void battleEnd(const BattleResult * br, QueryID queryID) override; void makeTurn(); diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 12d4021ca..69fde8c7b 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -652,26 +652,20 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat waitForAllDialogs(); } -void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) +void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) { EVENT_HANDLER_CALLED_BY_CLIENT; - bool autoBattleResultRefused = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2); - lastBattleArmies.first = army1; - lastBattleArmies.second = army2; - //quick combat with neutral creatures only - auto * army2_object = dynamic_cast(army2); - if((!autoBattleResultRefused && !allowBattleReplay && army2_object - && (army2_object->getOwner() == PlayerColor::UNFLAGGABLE || army2_object->getOwner() == PlayerColor::NEUTRAL) - && settings["adventure"]["quickCombat"].Bool()) - || settings["adventure"]["alwaysSkipCombat"].Bool()) + + bool useQuickCombat = settings["adventure"]["quickCombat"].Bool(); + bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool(); + + if ((replayAllowed && useQuickCombat) || forceQuickCombat) { autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI->initBattleInterface(env, cb); - autofightingAI->battleStart(army1, army2, int3(0,0,0), hero1, hero2, side); + autofightingAI->battleStart(army1, army2, tile, hero1, hero2, side, false); isAutoFightOn = true; cb->registerBattleInterface(autofightingAI); - // Player shouldn't be able to move on adventure map if quick combat is going - allowBattleReplay = true; } //Don't wait for dialogs when we are non-active hot-seat player @@ -843,13 +837,17 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) if(!battleInt) { - bool allowManualReplay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool(); - allowBattleReplay = false; + bool allowManualReplay = queryID != -1; + auto wnd = std::make_shared(*br, *this, allowManualReplay); - wnd->resultCallback = [=](ui32 selection) + + if (allowManualReplay) { - cb->selectionMade(selection, queryID); - }; + wnd->resultCallback = [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }; + } GH.windows().pushWindow(wnd); // #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. // Otherwise NewTurn causes freeze. diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 39a23c798..d4372ec5f 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -65,8 +65,6 @@ class CPlayerInterface : public CGameInterface, public IUpdateable int firstCall; int autosaveCount; - std::pair lastBattleArmies; - bool allowBattleReplay = false; std::list> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr) @@ -169,7 +167,7 @@ protected: // Call-ins from server, should not be called directly, but only via void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect void battleStacksAttacked(const std::vector & bsa, bool ranged) override; void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right - void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right + void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleUnitsChanged(const std::vector & units) override; void battleObstaclesChanged(const std::vector & obstacles) override; void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack diff --git a/client/Client.cpp b/client/Client.cpp index 5cee41b88..35e7c1649 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -580,7 +580,7 @@ void CClient::battleStarted(const BattleInfo * info) auto callBattleStart = [&](PlayerColor color, ui8 side) { if(vstd::contains(battleints, color)) - battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side); + battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); }; callBattleStart(leftSide.color, 0); diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 240106a31..ac99ee935 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -501,7 +501,7 @@ void BattleWindow::bAutofightf() auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); ai->initBattleInterface(owner.curInt->env, owner.curInt->cb); - ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide()); + ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide(), false); owner.curInt->autofightingAI = ai; owner.curInt->cb->registerBattleInterface(ai); diff --git a/config/schemas/settings.json b/config/schemas/settings.json index ef234a135..41d41644a 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -223,7 +223,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "alwaysSkipCombat", "borderScroll", "leftButtonDrag" ], + "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag" ], "properties" : { "heroMoveTime" : { "type" : "number", @@ -253,7 +253,7 @@ "type" : "boolean", "default" : true }, - "alwaysSkipCombat" : { + "forceQuickCombat" : { "type" : "boolean", "default" : false }, diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index c499207e0..5b052d3c8 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -168,13 +168,13 @@ void CAdventureAI::battleCatapultAttacked(const CatapultAttack & ca) } void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, - const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) + const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) { assert(!battleAI); assert(cbc); battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName()); battleAI->initBattleInterface(env, cbc); - battleAI->battleStart(army1, army2, tile, hero1, hero2, side); + battleAI->battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); } void CAdventureAI::battleStacksAttacked(const std::vector & bsa, bool ranged) diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 3b59b69b0..3ee27c21a 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -149,7 +149,7 @@ public: virtual void yourTacticPhase(int distance) override; virtual void battleNewRound(int round) override; virtual void battleCatapultAttacked(const CatapultAttack & ca) override; - virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; + virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; virtual void battleStacksAttacked(const std::vector & bsa, bool ranged) override; virtual void actionStarted(const BattleAction &action) override; virtual void battleNewRoundFirst(int round) override; diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index af2132060..8d85df35e 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -69,7 +69,7 @@ public: virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start - virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side){}; //called by engine when battle starts; side=0 - left, side=1 - right + virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right virtual void battleUnitsChanged(const std::vector & units){}; virtual void battleObstaclesChanged(const std::vector & obstacles){}; virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 861e45c52..b2d5cd3a8 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -207,6 +207,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const curB->round = -2; curB->activeStack = -1; curB->creatureBank = creatureBank; + curB->replayAllowed = false; if(town) { diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 7d1361fee..e875cadfa 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -36,6 +36,7 @@ public: const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege) int3 tile; //for background and bonuses bool creatureBank; //auxilary field, do not serialize + bool replayAllowed; std::vector stacks; std::vector > obstacles; SiegeInfo si; @@ -61,6 +62,10 @@ public: h & tacticsSide; h & tacticDistance; h & static_cast(*this); + if (version > 824) + h & replayAllowed; + else + replayAllowed = false; } ////////////////////////////////////////////////////////////////////////// diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 62e76c9a2..926d73b6c 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,7 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 824; +const ui32 SERIALIZATION_VERSION = 825; const ui32 MINIMAL_SERIALIZATION_VERSION = 824; const std::string SAVEGAME_MAGIC = "VCMISVG"; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 9df39108a..12d8d6716 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -607,9 +607,15 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; finishingBattle = std::make_unique(battleQuery, queriedPlayers); - auto battleDialogQuery = std::make_shared(this, gs->curB); - battleResult.data->queryID = battleDialogQuery->queryID; - queries.addQuery(battleDialogQuery); + // in battles against neutrals, 1st player can ask to replay battle manually + if (!gs->curB->sides[1].color.isValidPlayer()) + { + auto battleDialogQuery = std::make_shared(this, gs->curB); + battleResult.data->queryID = battleDialogQuery->queryID; + queries.addQuery(battleDialogQuery); + } + else + battleResult.data->queryID = -1; //set same battle result for all queries for(auto q : queries.allQueries()) @@ -620,6 +626,9 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con } sendAndApply(battleResult.data); //after this point casualties objects are destroyed + + if (battleResult.data->queryID == -1) + endBattleConfirm(gs->curB); } void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) @@ -2118,6 +2127,10 @@ void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const //send info about battles BattleStart bs; bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); + + auto lastBattleQuery = std::dynamic_pointer_cast(queries.topQuery(bs.info->sides[0].color)); + bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); + sendAndApply(&bs); } @@ -2587,39 +2600,39 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI heroes[0] = hero1; heroes[1] = hero2; - setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + auto lastBattleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); + //existing battle query for retying auto-combat - auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); - if(battleQuery) + if(lastBattleQuery) { for(int i : {0, 1}) { if(heroes[i]) { SetMana restoreInitialMana; - restoreInitialMana.val = battleQuery->initialHeroMana[i]; + restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; restoreInitialMana.hid = heroes[i]->id; sendAndApply(&restoreInitialMana); } } - battleQuery->bi = gs->curB; - battleQuery->result = std::nullopt; - battleQuery->belligerents[0] = gs->curB->sides[0].armyObject; - battleQuery->belligerents[1] = gs->curB->sides[1].armyObject; + lastBattleQuery->bi = gs->curB; + lastBattleQuery->result = std::nullopt; + lastBattleQuery->belligerents[0] = gs->curB->sides[0].armyObject; + lastBattleQuery->belligerents[1] = gs->curB->sides[1].armyObject; } - battleQuery = std::make_shared(this, gs->curB); + auto nextBattleQuery = std::make_shared(this, gs->curB); for(int i : {0, 1}) { if(heroes[i]) { - battleQuery->initialHeroMana[i] = heroes[i]->mana; + nextBattleQuery->initialHeroMana[i] = heroes[i]->mana; } } - queries.addQuery(battleQuery); + queries.addQuery(nextBattleQuery); this->battleThread = std::make_unique(boost::thread(&CGameHandler::runBattle, this)); } From 917229b9881a1e58959a6c027bd986c10ffd2300 Mon Sep 17 00:00:00 2001 From: heroesiiifan <77574150+heroesiiifan@users.noreply.github.com> Date: Fri, 28 Jul 2023 00:29:57 +0000 Subject: [PATCH 046/168] Update Languages.h --- lib/Languages.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Languages.h b/lib/Languages.h index 245430857..6bcd7c618 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -30,6 +30,7 @@ enum class ELanguages SWEDISH, TURKISH, UKRAINIAN, + VIETNAMESE, // Pseudo-languages, that have no translations but can define H3 encoding to use OTHER_CP1250, @@ -80,6 +81,7 @@ inline const auto & getLanguageList() { "swedish", "Swedish", "Svenska", "CP1252", "sv", true }, { "turkish", "Turkish", "Türkçe", "CP1254", "tr", true }, { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", true }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding { "other_cp1250", "Other (East European)", "", "CP1250", "", false }, { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false }, From 840d5afe179b8dd6b6b32be28607f117bb27f77f Mon Sep 17 00:00:00 2001 From: heroesiiifan <77574150+heroesiiifan@users.noreply.github.com> Date: Fri, 28 Jul 2023 00:31:07 +0000 Subject: [PATCH 047/168] Update languages.cpp --- launcher/languages.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/languages.cpp b/launcher/languages.cpp index 8c7174df3..670faa345 100644 --- a/launcher/languages.cpp +++ b/launcher/languages.cpp @@ -18,7 +18,7 @@ #include // list of language names, for generation of translations. Do not use directly, use Languages namespace instead -static const std::array languageTranslatedNamesGenerator = { +static const std::array languageTranslatedNamesGenerator = { { QT_TRANSLATE_NOOP("Language", "Czech"), QT_TRANSLATE_NOOP("Language", "Chinese"), @@ -36,6 +36,7 @@ static const std::array languageTranslatedNamesGenerator = { QT_TRANSLATE_NOOP("Language", "Swedish"), QT_TRANSLATE_NOOP("Language", "Turkish"), QT_TRANSLATE_NOOP("Language", "Ukrainian"), + QT_TRANSLATE_NOOP("Language", "Vietnamese"), QT_TRANSLATE_NOOP("Language", "Other (East European)"), QT_TRANSLATE_NOOP("Language", "Other (Cyrillic Script)"), QT_TRANSLATE_NOOP("Language", "Other (West European)"), From da7f1f4620d8fdb23c46c1823638712d985e1a2c Mon Sep 17 00:00:00 2001 From: heroesiiifan <77574150+heroesiiifan@users.noreply.github.com> Date: Fri, 28 Jul 2023 00:31:59 +0000 Subject: [PATCH 048/168] Update LanguageSettingDialog.java --- .../main/java/eu/vcmi/vcmi/settings/LanguageSettingDialog.java | 1 + 1 file changed, 1 insertion(+) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/LanguageSettingDialog.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/LanguageSettingDialog.java index 98bbcd9b2..aed0a60d3 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/LanguageSettingDialog.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/LanguageSettingDialog.java @@ -30,6 +30,7 @@ public class LanguageSettingDialog extends LauncherSettingDialog AVAILABLE_LANGUAGES.add("swedish"); AVAILABLE_LANGUAGES.add("turkish"); AVAILABLE_LANGUAGES.add("ukrainian"); + AVAILABLE_LANGUAGES.add("vietnamese"); AVAILABLE_LANGUAGES.add("other_cp1250"); AVAILABLE_LANGUAGES.add("other_cp1251"); AVAILABLE_LANGUAGES.add("other_cp1252"); From ddfeeb152d5d7833e9cf8e961b0cd42abde5de10 Mon Sep 17 00:00:00 2001 From: heroesiiifan <77574150+heroesiiifan@users.noreply.github.com> Date: Fri, 28 Jul 2023 00:33:42 +0000 Subject: [PATCH 049/168] Update settings.json --- config/schemas/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/schemas/settings.json b/config/schemas/settings.json index ef234a135..ca30b8c02 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -63,12 +63,12 @@ }, "language" : { "type" : "string", - "enum" : [ "english", "czech", "chinese", "finnish", "french", "german", "hungarian", "italian", "korean", "polish", "portuguese", "russian", "spanish", "swedish", "turkish", "ukrainian" ], + "enum" : [ "english", "czech", "chinese", "finnish", "french", "german", "hungarian", "italian", "korean", "polish", "portuguese", "russian", "spanish", "swedish", "turkish", "ukrainian", "vietnamese" ], "default" : "english" }, "gameDataLanguage" : { "type" : "string", - "enum" : [ "auto", "english", "czech", "chinese", "finnish", "french", "german", "hungarian", "italian", "korean", "polish", "portuguese", "russian", "spanish", "swedish", "turkish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ], + "enum" : [ "auto", "english", "czech", "chinese", "finnish", "french", "german", "hungarian", "italian", "korean", "polish", "portuguese", "russian", "spanish", "swedish", "turkish", "ukrainian", "vietnamese", "other_cp1250", "other_cp1251", "other_cp1252" ], "default" : "auto" }, "lastSave" : { From 648f7344c792eddd0d2865c493bb6cd121b89287 Mon Sep 17 00:00:00 2001 From: heroesiiifan <77574150+heroesiiifan@users.noreply.github.com> Date: Fri, 28 Jul 2023 00:34:28 +0000 Subject: [PATCH 050/168] Update mod.json --- config/schemas/mod.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/schemas/mod.json b/config/schemas/mod.json index 9c0d5b4e3..2c1fc8501 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -153,7 +153,7 @@ "language" : { "type" : "string", "description" : "Base language of the mod, before applying localizations. By default vcmi assumes English", - "enum" : [ "czech", "chinese", "english", "finnish", "french", "german", "hungarian", "italian", "korean", "polish", "portuguese", "russian", "spanish", "swedish", "turkish", "ukrainian" ] + "enum" : [ "czech", "chinese", "english", "finnish", "french", "german", "hungarian", "italian", "korean", "polish", "portuguese", "russian", "spanish", "swedish", "turkish", "ukrainian", "vietnamese" ] }, "czech" : { "$ref" : "#/definitions/localizable" @@ -203,6 +203,9 @@ "ukrainian" : { "$ref" : "#/definitions/localizable" }, + "vietnamese" : { + "$ref" : "#/definitions/localizable" + }, "translations" : { "type" : "array", "description" : "List of files with translations for this language", From bee59b3c1c9ccf3ad57cc080330163c7d771a24d Mon Sep 17 00:00:00 2001 From: heroesiiifan <77574150+heroesiiifan@users.noreply.github.com> Date: Fri, 28 Jul 2023 00:48:06 +0000 Subject: [PATCH 051/168] Update Languages.h --- lib/Languages.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Languages.h b/lib/Languages.h index 6bcd7c618..d00ce6ff7 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -63,7 +63,7 @@ struct Options inline const auto & getLanguageList() { - static const std::array languages + static const std::array languages { { { "czech", "Czech", "Čeština", "CP1250", "cs", true }, { "chinese", "Chinese", "简体中文", "GBK", "zh", true }, // Note: actually Simplified Chinese From 8c4e23f2d6ac32dbcc949d75072ba0a1e4493d18 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 27 Jul 2023 20:00:59 +0300 Subject: [PATCH 052/168] Updated Launcher localization --- launcher/translation/chinese.ts | 150 +++++----- launcher/translation/english.ts | 118 ++++---- launcher/translation/french.ts | 457 +++++------------------------- launcher/translation/german.ts | 150 +++++----- launcher/translation/polish.ts | 150 +++++----- launcher/translation/russian.ts | 150 +++++----- launcher/translation/spanish.ts | 150 +++++----- launcher/translation/ukrainian.ts | 154 +++++----- 8 files changed, 554 insertions(+), 925 deletions(-) diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 048c534c2..8ac241e96 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -407,120 +407,123 @@ CSettingsView - Open - 打开 - - - User data directory - 用户数据目录 - - - - - + + + Off 关闭 - - + + Artificial Intelligence 人工智能 - - + + Mod Repositories 模组仓库 - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + + Autosave limit (0 = off) + + + + Friendly AI in battles - + Framerate Limit - + + Autosave prefix + + + + + empty = map name prefix + + + + Refresh now - + Default repository - Update now - 立即更新 - - - - - + + + On 开启 - + Cursor 鼠标指针 - + Heroes III Data Language 英雄无敌3数据语言 - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -531,107 +534,95 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware 硬件 - + Software 软件 - + Heroes III Translation 发布版本里找不到这个项,不太清楚意义 英雄无敌3翻译 - + Check on startup 启动时检查更新 - + Fullscreen 全屏 - - + + General 通用设置 - + VCMI Language VCMI语言 - + Resolution 分辨率 - + Autosave 自动存档 - + Display index 显示器序号 - + Network port 网络端口 - Data Directories - 数据目录 - - - - + + Video 视频设置 - Log files directory - 日志文件目录 - - - + Show intro 显示开场动画 - Build version - 版本号 - - - + Active 激活 - + Disabled 禁用 - + Enable 启用 - + Not Installed 未安装 - + Install 安装 @@ -730,10 +721,6 @@ Heroes® of Might and Magic® III HD is currently not supported! Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - - Install support for playing Heroes III in resolutions higher than 800x600 - 安装英雄无敌3的800x600以上分辨率支持 - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team @@ -821,10 +808,6 @@ Heroes® of Might and Magic® III HD is currently not supported! Heroes III Translation 英雄无敌3翻译 - - High Definition Support - 高分辨率支持 - In The Wake of Gods @@ -923,21 +906,26 @@ Heroes® of Might and Magic® III HD is currently not supported! + Vietnamese + + + + Other (East European) - + Other (Cyrillic Script) - + Other (West European) - + Auto (%1) 自动 (%1) diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index aa772876c..9270e252a 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -406,108 +406,123 @@ CSettingsView - - - + + + Off - - + + Artificial Intelligence - - + + Mod Repositories - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + + Autosave limit (0 = off) + + + + Friendly AI in battles - + Framerate Limit - + + Autosave prefix + + + + + empty = map name prefix + + + + Refresh now - + Default repository - - - + + + On - + Cursor - + Heroes III Data Language - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -518,94 +533,94 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware - + Software - + Heroes III Translation - + Check on startup - + Fullscreen - - + + General - + VCMI Language - + Resolution - + Autosave - + Display index - + Network port - - + + Video - + Show intro - + Active - + Disabled - + Enable - + Not Installed - + Install @@ -883,21 +898,26 @@ Heroes® of Might and Magic® III HD is currently not supported! - Other (East European) + Vietnamese - Other (Cyrillic Script) + Other (East European) + Other (Cyrillic Script) + + + + Other (West European) - + Auto (%1) diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index c38c637ad..110fbcbbd 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -198,118 +198,6 @@ Version - - CModListModel - - - Translation - Traduction - - - - Town - Ville - - - - Test - Test - - - - Templates - Modèles - - - - Spells - Sorts - - - - Music - Musique - - - - Sounds - Sons - - - - Skills - Compétences - - - - - Other - Autre - - - - Objects - Objets - - - - - Mechanics - Mécanique - - - - - Interface - Interface - - - - Heroes - Héros - - - - - Graphical - Graphique - - - - Expansion - Extension - - - - Creatures - Créatures - - - - Artifacts - Artefacts - - - - AI - IA - - - - Name - Nom - - - - Type - Type - - - - Version - Version - - CModListView @@ -398,10 +286,6 @@ Install Installer - - %p% (%v KB out of %m KB) - %p% (%v Ko sur %m Ko) - Abort @@ -527,63 +411,43 @@ CSettingsView - Change - Changer - - - Open - Ouvrir - - - Adventure Map AI - IA de Carte d'Aventure - - - User data directory - Dossier de donnée utilisateur - - - - - + + + Off Désactivé - - + + Artificial Intelligence Intelligence Artificielle - - + + Mod Repositories Dépôts de Mod - Update now - Mettre à jour maintenant - - - - - + + + On Activé - + Enemy AI in battles IA ennemie dans les batailles - + Default repository Dépôt par défaut - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -600,191 +464,174 @@ Mode fenêtré sans bord - le jeu s"exécutera dans une fenêtre qui couvre Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écran et utilisera la résolution sélectionnée. - + Windowed Fenêtré - + Borderless fullscreen Fenêtré sans bord - + Exclusive fullscreen Plein écran exclusif - + Neutral AI in battles IA neutre dans les batailles - + + Autosave limit (0 = off) + + + + Adventure Map Enemies Ennemis de la carte d"aventure - + + Autosave prefix + + + + + empty = map name prefix + + + + Interface Scaling Mise à l"échelle de l"interface - + Cursor Curseur - + Heroes III Data Language Langue des Données de Heroes III - + Framerate Limit Limite de fréquence d"images - + Hardware Matériel - + Software Logiciel - + Heroes III Translation Traduction de Heroes III - + Adventure Map Allies Alliés de la carte d"aventure - + Additional repository Dépôt supplémentaire - + Check on startup Vérifier au démarrage - + Refresh now Actualiser maintenant - + Friendly AI in battles IA amicale dans les batailles - + Fullscreen Plein écran - Neutral AI - IA neutre - - - Real - Réel - - - - + + General Général - + VCMI Language Langue de VCMI - Friendly AI - IA amicale - - - + Resolution Résolution - + Autosave Sauvegarde automatique - + Display index Index d'affichage - + Network port Port de réseau - Data Directories - Dossier de Données - - - - + + Video Vidéo - Extra data directory - Dossier de données supplémentaires - - - Log files directory - Dossier de journalisation - - - + Show intro Montrer l'intro - Build version - Version de la construction - - - Enemy AI - IA ennemi - - - + Active Actif - + Disabled Désactivé - + Enable Activé - + Not Installed Pas Installé - + Install Installer @@ -816,110 +663,26 @@ Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écra Have a question? Found a bug? Want to help? Join us! Avez-vous une question ? Avez-vous trouvé un bogue ? Besoin d'aide ? Rejoignez-nous ! - - Thank you for installing VCMI! - - Before you can start playing, there are a few more steps that need to be completed. - - Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might - and Magic® III: Complete or The Shadow of Death. - - Heroes® of Might and Magic® III HD is currently not supported! - - Merci d'avoir installé VCMI ! - - Avant que vous ne commenciez à jouer, il y a encore quelques étapes qui ont besoin d'être complétées. - - Veuillez garder à l'esprit que pour utiliser VCMI, vous devez posséder les fichiers de données originaux de Heroes® of Might - and Magic® III: Complete ou The Shadow of Death. - - Heroes® of Might and Magic® III HD n'est pas supporté actuellement ! - - Locate Heroes III data files Localiser les fichiers de données de Heroes III - - If you don't have a copy of Heroes III installed, you can use our automatic installation tool - 'vcmibuilder', which only requires the GoG.com Heroes III installer. Please visit our wiki for - detailed instructions. - - Si vous n'avez pas de copie de Heroes III d'installé, vous pouvez utiliser notre - outil d'installation automatique 'vcmibuilder', qui ne nécessite que l'installeur Heroes III de GoG.com. - Veuillez visiter notre wiki pour plus d'informations. - - - - To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy - the Heroes III data to one of these directories. - - Pour lancer VCMI, les fichiers de données de Heroes III ont besoin d'être présent dans un des emplacements spécifiés. - Veuillez copier les données de Heroes III vers un de ces dossiers. - - - - Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy - the existing data automatically. - - Ou sinon, vous pouvez indiquer le dossier où les données de Heroes III sont installées et VCMI va copier les données existantes automatiquement. - - Your Heroes III data files have been successfully found. Vos fichiers de données de Heroes III ont été trouvés. - - The automatic detection of the Heroes III language has failed. Please select the language of your - Heroes III manually - - La détection automatique de la langue de Heroes III a échoué. Veuillez sélectionner - la langue de votre Heroes III manuellement - - Install a translation of Heroes III in your preferred language Installer une traduction de Heroes III dans la langue de votre choix - - Optionally, you can install additional mods either now, or at any point later, using the VCMI - Launcher - - Éventuellement, vous pouvez installer des mods supplémentaires soit maintenant, soit plus tard, en utilisant le lanceur VCMI - - - - Install support for playing Heroes III in resolutions higher than 800x600 - Installer un support pour jouer à Heroes III avec des résolutions supérieures à 800x600 - - - - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported - by the VCMI team - - Installer une version compatible de "Horn of the Abyss", une extension de - Heroes III fait par des fans porté par l'équipe VCMI - - - - Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion - - Installer une version compatible de "In The Wake of Gods", une extension de - Heroes III fait par des fans - - Finish Terminer - - Step %v out of %m - Étape %v sur %m - VCMI on Github @@ -1032,11 +795,7 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Interface Improvements - Améliorations de l'interface - - - High Definition Support - Support de Haute Définition + Améliorations de l'interface @@ -1051,7 +810,7 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - Installer le mod qui fournit diverses améliorations d'interface, telles qu'une meilleure interface pour les cartes aléatoires et des actions sélectionnables dans les batailles + Installer le mod qui fournit diverses améliorations d'interface, telles qu'une meilleure interface pour les cartes aléatoires et des actions sélectionnables dans les batailles @@ -1156,21 +915,26 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge + Vietnamese + + + + Other (East European) Autre (Europe de l'Est) - + Other (Cyrillic Script) Autre (Alphabet Cyrillique) - + Other (West European) Autre (Europe de l'Ouest) - + Auto (%1) Auto (%1) @@ -1335,85 +1099,6 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Démarrer une partie - - Salon - - Connect - Connecter - - - Username - Nom d'utilisateur - - - Server - Serveur - - - Lobby chat - Discussion de salle d'attente - - - Session - Session - - - Players - Joueurs - - - Resolve - Résoudre - - - New game - Nouvelle partie - - - Load game - Charger une partie - - - New room - Nouveau salon - - - Players in lobby - Joueurs à la salle d'attente - - - Join room - Rejoindre le salon - - - Ready - Prêt - - - Mods mismatch - Incohérence de mods - - - Leave - Quitter - - - Kick player - Jeter le joueur - - - Players in the room - Joueurs dans le salon - - - Disconnect - Déconnecter - - - No issues detected - Pas de problème détecté - - UpdateDialog diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index 6dd2341cb..b28163a84 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -406,120 +406,123 @@ CSettingsView - Open - Öffnen - - - User data directory - Verzeichnis der Benutzerdaten - - - - - + + + Off Aus - - + + Artificial Intelligence Künstliche Intelligenz - - + + Mod Repositories Mod-Repositorien - + Interface Scaling Skalierung der Benutzeroberfläche - + Neutral AI in battles Neutrale KI in Kämpfen - + Enemy AI in battles Gegnerische KI in Kämpfen - + Additional repository Zusätzliches Repository - + Adventure Map Allies Abenteuerkarte Verbündete - + Adventure Map Enemies Abenteuerkarte Feinde - + Windowed Fenstermodus - + Borderless fullscreen Randloser Vollbildmodus - + Exclusive fullscreen Exklusiver Vollbildmodus - + + Autosave limit (0 = off) + + + + Friendly AI in battles Freundliche KI in Kämpfen - + Framerate Limit Limit der Bildrate - + + Autosave prefix + + + + + empty = map name prefix + + + + Refresh now Jetzt aktualisieren - + Default repository Standard Repository - Update now - Jetzt aktualisieren - - - - - + + + On An - + Cursor Zeiger - + Heroes III Data Language Sprache der Heroes III Daten - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -536,106 +539,94 @@ Randloser Fenstermodus - das Spiel läuft in einem Fenster, das den gesamten Bil Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwendet die gewählte Auflösung. - + Hardware Hardware - + Software Software - + Heroes III Translation Heroes III Übersetzung - + Check on startup Beim Start prüfen - + Fullscreen Vollbild - - + + General Allgemein - + VCMI Language VCMI-Sprache - + Resolution Auflösung - + Autosave Autospeichern - + Display index Anzeige-Index - + Network port Netzwerk-Port - Data Directories - Daten-Verzeichnisse - - - - + + Video Video - Log files directory - Verzeichnis der Log-Dateien - - - + Show intro Intro anzeigen - Build version - Version des Builds - - - + Active Aktiv - + Disabled Deaktiviert - + Enable Aktivieren - + Not Installed Nicht installiert - + Install Installieren @@ -734,10 +725,6 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles Installiere Mod, die verschiedene Interface-Verbesserungen bietet, wie z.B. ein besseres Interface für zufällige Karten und wählbare Aktionen in Kämpfen - - Install support for playing Heroes III in resolutions higher than 800x600 - Installieren Sie Unterstützung für das Spielen von Heroes III in anderen Auflösungen als 800x600 - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team @@ -825,10 +812,6 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Heroes III Translation Heroes III Übersetzung - - High Definition Support - Unterstützung für hohe Auflösungen - In The Wake of Gods @@ -927,21 +910,26 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! + Vietnamese + + + + Other (East European) Sonstige (osteuropäisch) - + Other (Cyrillic Script) Sonstige (kyrillische Schrift) - + Other (West European) Sonstige (westeuropäisch) - + Auto (%1) Auto (%1) diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 62ad8bb1c..72bfa264a 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -406,120 +406,123 @@ CSettingsView - Open - Otwórz - - - User data directory - Katalog danych użytkownika - - - - - + + + Off Wyłączony - - + + Artificial Intelligence Sztuczna Inteligencja - - + + Mod Repositories Repozytoria modów - + Interface Scaling Skala interfejsu - + Neutral AI in battles AI bitewne jednostek neutralnych - + Enemy AI in battles AI bitewne wrogów - + Additional repository Dodatkowe repozytorium - + Adventure Map Allies AI sojuszników mapy przygody - + Adventure Map Enemies AI wrogów mapy przygody - + Windowed Okno - + Borderless fullscreen Pełny ekran (tryb okna) - + Exclusive fullscreen Pełny ekran klasyczny - + + Autosave limit (0 = off) + + + + Friendly AI in battles AI bitewne sojuszników - + Framerate Limit Limit FPS - + + Autosave prefix + + + + + empty = map name prefix + + + + Refresh now Odśwież - + Default repository Domyślne repozytorium - Update now - Zaktualizuj teraz - - - - - + + + On Włączony - + Cursor Kursor - + Heroes III Data Language Język plików Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -536,106 +539,94 @@ Pełny ekran w trybie okna - gra uruchomi się w oknie przysłaniającym cały e Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybranej przez ciebie rozdzielczości ekranu. - + Hardware Sprzętowy - + Software Programowy - + Heroes III Translation Tłumaczenie Heroes III - + Check on startup Sprawdzaj przy uruchomieniu - + Fullscreen Pełny ekran - - + + General Ogólne - + VCMI Language Język VCMI - + Resolution Rozdzielczość - + Autosave Autozapis - + Display index Numer wyświetlacza - + Network port Port sieciowy - Data Directories - Katalogi z danymi - - - - + + Video Obraz - Log files directory - Katalog logów - - - + Show intro Pokaż intro - Build version - Wersja programu - - - + Active Aktywny - + Disabled Wyłączone - + Enable Włącz - + Not Installed Nie zainstalowano - + Install Zainstaluj @@ -734,10 +725,6 @@ Heroes III: HD Edition nie jest obecnie wspierane! Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - - Install support for playing Heroes III in resolutions higher than 800x600 - Zainstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600 - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team @@ -825,10 +812,6 @@ Heroes III: HD Edition nie jest obecnie wspierane! Heroes III Translation Tłumaczenie Heroes III - - High Definition Support - Wsparcie High Definition - In The Wake of Gods @@ -927,21 +910,26 @@ Heroes III: HD Edition nie jest obecnie wspierane! - Other (East European) + Vietnamese - Other (Cyrillic Script) + Other (East European) + Other (Cyrillic Script) + + + + Other (West European) - + Auto (%1) diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index cfb060df8..7e0953a29 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -406,141 +406,144 @@ CSettingsView - Open - Открыть - - - User data directory - Данные пользователя - - - + Interface Scaling - - - + + + Off Отключено - - - + + + On Включено - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Check on startup Проверять при запуске - + Fullscreen Полноэкранный режим - - + + General Общее - + VCMI Language Язык VCMI - + Cursor Курсор - - + + Artificial Intelligence Искусственный интеллект - - + + Mod Repositories Репозитории модов - + Adventure Map Allies - + Refresh now - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + + Autosave limit (0 = off) + + + + Friendly AI in battles - + Framerate Limit - + + Autosave prefix + + + + + empty = map name prefix + + + + Default repository - Update now - Обновить сейчас - - - + Heroes III Data Language Язык данных Героев III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -551,85 +554,73 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware Аппаратный - + Software Программный - + Heroes III Translation Перевод Героев III - + Resolution Разрешение экрана - + Autosave Автосохранение - + Display index Дисплей - + Network port Сетевой порт - Data Directories - Директории данных - - - - + + Video Графика - Log files directory - Журналы - - - + Show intro Вступление - Build version - Версия сборки - - - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить @@ -728,10 +719,6 @@ Heroes® of Might and Magic® III HD is currently not supported! Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - - Install support for playing Heroes III in resolutions higher than 800x600 - Установить поддержку запуска Героев III в разрешениях, отличных от 800x600 - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team @@ -819,10 +806,6 @@ Heroes® of Might and Magic® III HD is currently not supported! Heroes III Translation Перевод Героев III - - High Definition Support - Поддержка высоких разрешений - In The Wake of Gods @@ -921,21 +904,26 @@ Heroes® of Might and Magic® III HD is currently not supported! + Vietnamese + + + + Other (East European) Другой (восточноевропейский) - + Other (Cyrillic Script) Другой (кириллический) - + Other (West European) Другой (западноевропейский) - + Auto (%1) Авто (%1) diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index eca998ccc..6401189bc 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -406,166 +406,165 @@ CSettingsView - Open - Abrir - - - User data directory - Directorio de datos del usuario - - - - - + + + Off Desactivado - - + + Artificial Intelligence Inteligencia Artificial - - + + Mod Repositories Repositorios de Mods - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + + Autosave limit (0 = off) + + + + Friendly AI in battles - + Framerate Limit - + + Autosave prefix + + + + + empty = map name prefix + + + + Refresh now - + Default repository - Update now - Actualizar ahora - - - - - + + + On Encendido - + Cursor Cursor - + Heroes III Translation Traducción de Heroes III - + Fullscreen Pantalla completa - - + + General General - + VCMI Language Idioma de VCMI - + Resolution Resolución - + Autosave Autoguardado - + Display index Mostrar índice - + Network port Puerto de red - Data Directories - Directorios de datos - - - - + + Video Vídeo - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -576,60 +575,52 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware Hardware - + Software Software - Log files directory - Directorio de archivos de registro - - - + Show intro Mostrar introducción - Build version - Versión - - - + Check on startup Comprovar al inicio - + Heroes III Data Language Idioma de los datos de Heroes III. - + Active Activado - + Disabled Desactivado - + Enable Activar - + Not Installed No Instalado - + Install Instalar @@ -790,10 +781,6 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Heroes III Translation Traducción de Heroes III. - - High Definition Support - Soporte para resoluciones en Alta Definición - In The Wake of Gods @@ -809,10 +796,6 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher Opcionalmente, puedes instalar mods adicionales ya sea ahora o en cualquier momento posterior, utilizando el lanzador de VCMI. - - Install support for playing Heroes III in resolutions higher than 800x600 - Instalar soporte para jugar Heroes III en resoluciones superiores a 800x600 - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team @@ -921,21 +904,26 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi + Vietnamese + + + + Other (East European) Otro (Europa del Este) - + Other (Cyrillic Script) - + Other (West European) - + Auto (%1) Automático (%1) diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index 69f1f51c9..b4362d867 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -406,124 +406,123 @@ CSettingsView - Open - Відкрити - - - User data directory - Тека даних користувача - - - - - + + + Off Вимкнено - - + + Artificial Intelligence Штучний інтелект - - + + Mod Repositories Репозиторії модифікацій - + Interface Scaling Масштабування інтерфейсу - + Neutral AI in battles Нейтральний ШІ в боях - + Enemy AI in battles Ворожий ШІ в боях - + Additional repository Додатковий репозиторій - Game data directory - Тека даних гри - - - + Adventure Map Allies Союзники на мапі пригод - + Adventure Map Enemies Вороги на мапі пригод - + Windowed У вікні - + Borderless fullscreen Повноекранне вікно - + Exclusive fullscreen Повноекранний (ексклюзивно) - + + Autosave limit (0 = off) + Кількість автозбережень + + + Friendly AI in battles Дружній ШІ в боях - + Framerate Limit Обмеження частоти кадрів - + + Autosave prefix + Префікс назв автозбережень + + + + empty = map name prefix + (використовувати назву карти) + + + Refresh now Оновити зараз - + Default repository Стандартний репозиторій - Update now - Оновити зараз - - - - - + + + On Увімкнено - + Cursor Курсор - + Heroes III Data Language Мова Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -540,106 +539,94 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Повноекранний ексклюзивний режим - гра займатиме весь екран і використовуватиме вибрану роздільну здатність. - + Hardware Апаратний - + Software Програмний - + Heroes III Translation Переклад Heroes III - + Check on startup Перевіряти на старті - + Fullscreen Повноекранний режим - - + + General Загальні налаштування - + VCMI Language Мова VCMI - + Resolution Роздільна здатність - + Autosave Автозбереження - + Display index Дісплей - + Network port Мережевий порт - Data Directories - Теки даних гри - - - - + + Video Графіка - Log files directory - Тека файлів журналу - - - + Show intro Вступні відео - Build version - Версія збірки - - - + Active Активні - + Disabled Деактивований - + Enable Активувати - + Not Installed Не встановлено - + Install Встановити @@ -738,10 +725,6 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles Встановити різноманітні покращення інтерфейсу, такі як покращений інтерфейс випадкових карт та вибір варіантів дій у боях - - Install support for playing Heroes III in resolutions higher than 800x600 - Встановити підтримку для гри в Heroes III у роздільних здатностях, більших за 800x600 - Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team @@ -829,10 +812,6 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Heroes III Translation Переклад Heroes III - - High Definition Support - Підтримка високих роздільних здатностей - In The Wake of Gods @@ -931,21 +910,26 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс + Vietnamese + В'єтнамська + + + Other (East European) Інша (східноєвропейська) - + Other (Cyrillic Script) Інша (кирилиця) - + Other (West European) Інша (західноєвропейська) - + Auto (%1) Авто (%1) From 4ba6806a690c1766460e0ba4b9d7be4f80728448 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 27 Jul 2023 20:01:20 +0300 Subject: [PATCH 053/168] Added missing string to Editor translation --- mapeditor/mainwindow.cpp | 12 +++++------ mapeditor/mapview.cpp | 2 +- mapeditor/validator.cpp | 42 +++++++++++++++++++------------------- mapeditor/windownewmap.cpp | 4 ++-- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index f1c4f9f5b..156bcdeb4 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -249,7 +249,7 @@ bool MainWindow::getAnswerAboutUnsavedChanges() { if(unsaved) { - auto sure = QMessageBox::question(this, "Confirmation", "Unsaved changes will be lost, are you sure?"); + auto sure = QMessageBox::question(this, tr("Confirmation"), tr("Unsaved changes will be lost, are you sure?")); if(sure == QMessageBox::No) { return false; @@ -326,7 +326,7 @@ bool MainWindow::openMap(const QString & filenameSelect) if(!CResourceHandler::get("mapEditor")->existsResource(resId)) { - QMessageBox::warning(this, "Failed to open map", "Cannot open map from this folder"); + QMessageBox::warning(this, tr("Failed to open map"), tr("Cannot open map from this folder")); return false; } @@ -1125,11 +1125,11 @@ void MainWindow::on_actionUpdate_appearance_triggered() if(controller.scene(mapLevel)->selectionObjectsView.getSelection().empty()) { - QMessageBox::information(this, "Update appearance", "No objects selected"); + QMessageBox::information(this, tr("Update appearance"), tr("No objects selected")); return; } - if(QMessageBox::Yes != QMessageBox::question(this, "Update appearance", "This operation is irreversible. Do you want to continue?")) + if(QMessageBox::Yes != QMessageBox::question(this, tr("Update appearance"), tr("This operation is irreversible. Do you want to continue?"))) return; controller.scene(mapLevel)->selectionTerrainView.clear(); @@ -1188,7 +1188,7 @@ void MainWindow::on_actionUpdate_appearance_triggered() if(errors) - QMessageBox::warning(this, "Update appearance", QString("Errors occured. %1 objects were not updated").arg(errors)); + QMessageBox::warning(this, tr("Update appearance"), QString(tr("Errors occured. %1 objects were not updated")).arg(errors)); } @@ -1228,7 +1228,7 @@ void MainWindow::on_actionPaste_triggered() void MainWindow::on_actionExport_triggered() { - QString fileName = QFileDialog::getSaveFileName(this, "Save to image", QCoreApplication::applicationDirPath(), "BMP (*.bmp);;JPEG (*.jpeg);;PNG (*.png)"); + QString fileName = QFileDialog::getSaveFileName(this, tr("Save to image"), QCoreApplication::applicationDirPath(), "BMP (*.bmp);;JPEG (*.jpeg);;PNG (*.png)"); if(!fileName.isNull()) { QImage image(ui->mapView->scene()->sceneRect().size().toSize(), QImage::Format_RGB888); diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index af7bffe35..87775258a 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -468,7 +468,7 @@ void MapView::dropEvent(QDropEvent * event) else { controller->discardObject(sc->level); - QMessageBox::information(this, "Can't place object", errorMsg); + QMessageBox::information(this, tr("Can't place object"), errorMsg); } } diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index 9091da7ef..d1ded12b0 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -47,7 +47,7 @@ std::list Validator::validate(const CMap * map) if(!map) { - issues.emplace_back("Map is not loaded", true); + issues.emplace_back(tr("Map is not loaded"), true); return issues; } @@ -67,14 +67,14 @@ std::list Validator::validate(const CMap * map) if(p.canHumanPlay) ++hplayers; if(p.allowedFactions.empty()) - issues.emplace_back(QString("No factions allowed for player %1").arg(i), true); + issues.emplace_back(QString(tr("No factions allowed for player %1")).arg(i), true); } if(hplayers + cplayers == 0) - issues.emplace_back("No players allowed to play this map", true); + issues.emplace_back(tr("No players allowed to play this map"), true); if(hplayers + cplayers == 1) - issues.emplace_back("Map is allowed for one player and cannot be started", true); + issues.emplace_back(tr("Map is allowed for one player and cannot be started"), true); if(!hplayers) - issues.emplace_back("No human players allowed to play this map", true); + issues.emplace_back(tr("No human players allowed to play this map"), true); std::set allHeroesOnMap; //used to find hero duplicated @@ -90,13 +90,13 @@ std::list Validator::validate(const CMap * map) dynamic_cast(o.get()) || dynamic_cast(o.get())) { - issues.emplace_back(QString("Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner").arg(o->instanceName.c_str()), true); + issues.emplace_back(QString(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("Object %1 is assigned to non-playable player %2").arg(o->instanceName.c_str(), o->getOwner().getStr().c_str()), true); + issues.emplace_back(QString(tr("Object %1 is assigned to non-playable player %2")).arg(o->instanceName.c_str(), o->getOwner().getStr().c_str()), true); } //checking towns if(auto * ins = dynamic_cast(o.get())) @@ -113,24 +113,24 @@ std::list Validator::validate(const CMap * map) if(ins->ID == Obj::PRISON) { if(ins->getOwner() != PlayerColor::NEUTRAL) - issues.emplace_back(QString("Prison %1 must be a NEUTRAL").arg(ins->instanceName.c_str()), true); + issues.emplace_back(QString(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("Hero %1 must have an owner").arg(ins->instanceName.c_str()), true); + issues.emplace_back(QString(tr("Hero %1 must have an owner")).arg(ins->instanceName.c_str()), true); } if(ins->type) { if(!map->allowedHeroes[ins->type->getId().getNum()]) - issues.emplace_back(QString("Hero %1 is prohibited by map settings").arg(ins->type->getNameTranslated().c_str()), false); + issues.emplace_back(QString(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("Hero %1 has duplicate on map").arg(ins->type->getNameTranslated().c_str()), false); + issues.emplace_back(QString(tr("Hero %1 has duplicate on map")).arg(ins->type->getNameTranslated().c_str()), false); } else - issues.emplace_back(QString("Hero %1 has an empty type and must be removed").arg(ins->instanceName.c_str()), true); + issues.emplace_back(QString(tr("Hero %1 has an empty type and must be removed")).arg(ins->instanceName.c_str()), true); } //checking for arts @@ -141,16 +141,16 @@ std::list Validator::validate(const CMap * map) if(ins->storedArtifact) { if(!map->allowedSpells[ins->storedArtifact->getId().getNum()]) - issues.emplace_back(QString("Spell scroll %1 is prohibited by map settings").arg(ins->getObjectName().c_str()), false); + issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->getObjectName().c_str()), false); } else - issues.emplace_back(QString("Spell scroll %1 doesn't have instance assigned and must be removed").arg(ins->instanceName.c_str()), true); + issues.emplace_back(QString(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[ins->subID]) { - issues.emplace_back(QString("Artifact %1 is prohibited by map settings").arg(ins->getObjectName().c_str()), false); + issues.emplace_back(QString(tr("Artifact %1 is prohibited by map settings")).arg(ins->getObjectName().c_str()), false); } } } @@ -159,30 +159,30 @@ std::list Validator::validate(const CMap * map) //verification of starting towns for(auto & mp : amountOfCastles) if(mp.second == 0) - issues.emplace_back(QString("Player %1 doesn't have any starting town").arg(mp.first), false); + issues.emplace_back(QString(tr("Player %1 doesn't have any starting town")).arg(mp.first), false); //verification of map name and description if(map->name.empty()) - issues.emplace_back("Map name is not specified", false); + issues.emplace_back(tr("Map name is not specified"), false); if(map->description.empty()) - issues.emplace_back("Map description is not specified", false); + issues.emplace_back(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("Map contains object from mod \"%1\", but doesn't require it").arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).name)), true); + issues.emplace_back(QString(tr("Map contains object from mod \"%1\", but doesn't require it")).arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).name)), true); } } } catch(const std::exception & e) { - issues.emplace_back(QString("Exception occurs during validation: %1").arg(e.what()), true); + issues.emplace_back(QString(tr("Exception occurs during validation: %1")).arg(e.what()), true); } catch(...) { - issues.emplace_back("Unknown exception occurs during validation", true); + issues.emplace_back(tr("Unknown exception occurs during validation"), true); } return issues; diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index 0ebe8d806..6fdd404b8 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -268,7 +268,7 @@ void WindowNewMap::on_okButton_clicked() //verify map template if(mapGenOptions.getPossibleTemplates().empty()) { - QMessageBox::warning(this, "No template", "No template for parameters scecified. Random map cannot be generated."); + QMessageBox::warning(this, tr("No template"), tr("No template for parameters scecified. Random map cannot be generated.")); return; } @@ -288,7 +288,7 @@ void WindowNewMap::on_okButton_clicked() } catch(const std::exception & e) { - QMessageBox::critical(this, "RMG failure", e.what()); + QMessageBox::critical(this, tr("RMG failure"), e.what()); } } else From b52a21f8eedb70a105067d5ec3e1325743847e13 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 27 Jul 2023 20:12:59 +0300 Subject: [PATCH 054/168] Regenerated map editor translations to include missing strings --- mapeditor/translation/english.ts | 423 +++++++++++++++++++---------- mapeditor/translation/french.ts | 395 ++++++++++++++++++--------- mapeditor/translation/german.ts | 399 ++++++++++++++++++--------- mapeditor/translation/polish.ts | 399 ++++++++++++++++++--------- mapeditor/translation/russian.ts | 399 ++++++++++++++++++--------- mapeditor/translation/spanish.ts | 401 ++++++++++++++++++--------- mapeditor/translation/ukrainian.ts | 403 ++++++++++++++++++--------- 7 files changed, 1906 insertions(+), 913 deletions(-) diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 0fe684d69..1c026f635 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -40,284 +40,327 @@ - + Map - + Edit - + View - + Player - + Toolbar - + Minimap - + Map Objects View - + Browser - + Inspector - + Property - + Value - + Terrains View - + Brush - + Terrains - + Roads - + Rivers - + Open - + Save - + New - + Save as... - + Ctrl+Shift+S - + U/G - - + + View underground - + Pass - + Cut - + Copy - + Paste - + Fill - + Fills the selection with obstacles - + Grid - + General - + Map title and description - + Players settings - - + + Undo - + Redo - + Erase - + Neutral - + Validate - + + + + Update appearance - + Recreate obstacles - + Player 1 - + Player 2 - + Player 3 - + Player 4 - + Player 5 - + Player 6 - + Player 7 - + Player 8 - + Export as... - + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + + + + Failed to open map + + + + + Cannot open map from this folder + + + + Open map - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) - - + + Save map - - + + VCMI maps (*.vmap) - + Type - + View surface + + + No objects selected + + + + + This operation is irreversible. Do you want to continue? + + + + + Errors occured. %1 objects were not updated + + + + + Save to image + + MapSettings @@ -327,162 +370,220 @@ - + General - + Map name - + Map description - + + Limit maximum heroes level + + + + Difficulty - - Events + + Mods - - Victory - - - - - Victory message - - - - - Only for human players + + Mandatory mods for playing this map - Allow standard victory + Mod name - - - Parameters + + Version - - Loss + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + Full content mods + + + + + Events + + + + + Victory + + + + + Victory message + + + + + Only for human players + + + + + Allow standard victory + + + + + + Parameters + + + + + Loss + + + + 7 days without town - + Defeat message - + Abilities - + Spells - + Artifacts - + Heroes - + Ok - + No special victory - + Capture artifact - + Hire creatures - + Accumulate resources - + Construct building - + Capture town - + Defeat hero - + Transport artifact - + No special loss - + Lose castle - + Lose hero - + Time expired - + Days without town + + MapView + + + Can't place object + + + MessageWidget @@ -494,47 +595,47 @@ PlayerParams - - No team - - - - + Human/CPU - + CPU only - + Team - + Main town - + + Color + + + + Random faction - + Generate hero at main - + (default) - + Player ID: %1 @@ -552,7 +653,12 @@ - + + 1 + + + + Ok @@ -608,6 +714,11 @@ Map is not loaded + + + No factions allowed for player %1 + + No players allowed to play this map @@ -693,6 +804,11 @@ Map description is not specified + + + Map contains object from mod "%1", but doesn't require it + + Exception occurs during validation: %1 @@ -762,114 +878,139 @@ - + 0 - + Human/Computer - - - - + + + + Random - + Computer only - + + Human teams + + + + + Computer teams + + + + Monster strength - + Weak - - + + Normal - + Strong - + Water content - + None - + Islands - + Template - + Custom seed - + Generate random map - + Ok - + Cancel + + + No template + + + + + No template for parameters scecified. Random map cannot be generated. + + + + + RMG failure + + main - + Filepath of the map to open. - + Extract original H3 archives into a separate folder. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. - + Delete original files, for the ones splitted / converted. diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 4f67ba0b7..65c77b0fa 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -27,8 +27,6 @@ Générer une carte - - MainWindow @@ -42,284 +40,327 @@ Fichier - + Map Carte - + Edit Édition - + View Affichage - + Player Joueur - + Toolbar Barre d'outils - + Minimap Mini-carte - + Map Objects View Vue des objets cartographiques - + Browser Navigateur - + Inspector Inspecteur - + Property Propriété - + Value Valeur - + Terrains View Vue des terrains - + Brush Pinceau - + Terrains Terrains - + Roads Routes - + Rivers Rivières - + Open Ouvrir - + Save Enregistrer - + New Nouveau - + Save as... Enregistrer sous... - + Ctrl+Shift+S Ctrl+Maj+S - + U/G Sous-sol/Surface - - + + View underground Voir le sous-sol - + Pass Passage - + Cut Couper - + Copy Copier - + Paste Coller - + Fill Remplir - + Fills the selection with obstacles Remplir la sélection d'obstacles - + Grid Grille - + General Général - + Map title and description Titre et description de la carte - + Players settings Paramètres des joueurs - - + + Undo Annuler - + Redo Rétablir - + Erase Effacer - + Neutral Neutre - + Validate Valider - + + + + Update appearance Mettre à jour l'apparence - + Recreate obstacles Recréer des obstacles - + Player 1 Joueur 1 - + Player 2 Joueur 2 - + Player 3 Joueur 3 - + Player 4 Joueur 4 - + Player 5 Joueur 5 - + Player 6 Joueur 6 - + Player 7 Joueur 7 - + Player 8 Joueur 8 - + Export as... Exporter sous... - + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + + + + Failed to open map + + + + + Cannot open map from this folder + + + + Open map Ouvrir la carte - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Toutes les cartes prises en charge (*.vmap *.h3m);;Cartes VCMI (*.vmap);;Cartes HoMM3 (*.h3m) - - + + Save map Enregistrer la carte - - + + VCMI maps (*.vmap) Cartes VCMI (*.vmap) - + Type Type - + View surface Afficher la surface + + + No objects selected + + + + + This operation is irreversible. Do you want to continue? + + + + + Errors occured. %1 objects were not updated + + + + + Save to image + + MapSettings @@ -329,162 +370,220 @@ Paramètres de la carte - + General Général - + Map name Nom de la carte - + Map description Description de la carte - + + Limit maximum heroes level + + + + Difficulty Difficulté - + + Mods + + + + + Mandatory mods for playing this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + Events Événements - + Victory Victoire - + Victory message Message de victoire - + Only for human players Uniquement pour les joueurs humains - + Allow standard victory Autoriser la victoire standard - - + + Parameters Paramètres - + Loss Perte - + 7 days without town 7 jours sans ville - + Defeat message Message de défaite - + Abilities Capacités - + Spells Sorts - + Artifacts Artefacts - + Heroes Héros - + Ok OK - + No special victory Pas de victoire spéciale - + Capture artifact Récupérer l'artefact - + Hire creatures Engagez des créatures - + Accumulate resources Accumuler des ressources - + Construct building Construire un bâtiment - + Capture town Conquérir une ville - + Defeat hero Battre un héros - + Transport artifact Transporter un artefact - + No special loss Aucune perte spéciale - + Lose castle Perdre un château - + Lose hero Perdre un héros - + Time expired Délai expiré - + Days without town Jours sans ville + + MapView + + + Can't place object + + + MessageWidget @@ -496,47 +595,51 @@ PlayerParams - No team - Aucune équipe + Aucune équipe - + Human/CPU Human/Ordinateur - + CPU only Ordinateur uniquement - + Team Équipe - + Main town Ville principale - + + Color + + + + Random faction Faction aléatoire - + Generate hero at main Générer un héros dans le principal - + (default) (par défaut) - + Player ID: %1 Identifiant du joueur : %1 @@ -554,7 +657,12 @@ Joueurs - + + 1 + 1 + + + Ok OK @@ -610,6 +718,11 @@ Map is not loaded Aucune carte n'est chargée + + + No factions allowed for player %1 + + No players allowed to play this map @@ -695,6 +808,11 @@ Map description is not specified La description de la carte n'est pas spécifiée + + + Map contains object from mod "%1", but doesn't require it + + Exception occurs during validation: %1 @@ -764,124 +882,139 @@ Joueurs - + 0 0 - + Human/Computer Humain/Ordinateur - - - - + + + + Random Aléatoire - + Computer only Ordinateur uniquement - + Human teams Équipes humaines - + Computer teams Équipes d'ordinateur - + Monster strength Force des monstres - + Weak Faible - - + + Normal Normale - + Strong Forte - + Water content Proportion en eau - + None Aucune - + Islands Îles - + Template Modèle - + Custom seed Graine personnalisée - + Generate random map Générer une carte aléatoire - + Ok OK - + Cancel Annuler + + + No template + + + + + No template for parameters scecified. Random map cannot be generated. + + + + + RMG failure + + main - + Filepath of the map to open. Chemin du fichier de la carte à ouvrir. - + Extract original H3 archives into a separate folder. Extraire les archives H3 d'origine dans un dossier séparé. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. À partir d'une archive extraite, il divise TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 et Un44 en fichiers PNG individuels. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. À partir d'une archive extraite, convertit des images uniques (trouvées dans le dossier Images) de .pcx en png. - + Delete original files, for the ones splitted / converted. Supprimer les fichiers d'origine, pour ceux fractionnés/convertis. diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index bcc5cf31b..61ca61ce6 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -40,284 +40,327 @@ Datei - + Map Karte - + Edit Bearbeiten - + View Ansicht - + Player Spieler - + Toolbar Werkzeugleiste - + Minimap Minikarte - + Map Objects View Kartenobjekte-Ansicht - + Browser Browser - + Inspector Inspektor - + Property Eigenschaft - + Value Wert - + Terrains View Terrain-Ansicht - + Brush Pinsel - + Terrains Terrains - + Roads Straßen - + Rivers Flüsse - + Open Öffnen - + Save Speichern - + New Neu - + Save as... Speichern unter... - + Ctrl+Shift+S Strg+Shift+S - + U/G U/G - - + + View underground Ansicht Untergrund - + Pass Passierbar - + Cut Ausschneiden - + Copy Kopieren - + Paste Einfügen - + Fill Füllen - + Fills the selection with obstacles Füllt die Auswahl mit Hindernissen - + Grid Raster - + General Allgemein - + Map title and description Titel und Beschreibung der Karte - + Players settings Spieler-Einstellungen - - + + Undo Rückgängig - + Redo Wiederholen - + Erase Löschen - + Neutral Neutral - + Validate Validieren - + + + + Update appearance Aussehen aktualisieren - + Recreate obstacles Hindernisse neu erschaffen - + Player 1 Spieler 1 - + Player 2 Spieler 2 - + Player 3 Spieler 3 - + Player 4 Spieler 4 - + Player 5 Spieler 5 - + Player 6 Spieler 6 - + Player 7 Spieler 7 - + Player 8 Spieler 8 - + Export as... Exportieren als... - + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + + + + Failed to open map + + + + + Cannot open map from this folder + + + + Open map Karte öffnen - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Alle unterstützten Karten (*.vmap *.h3m);;VCMI-Karten (*.vmap);;HoMM3-Karten (*.h3m) - - + + Save map Karte speichern - - + + VCMI maps (*.vmap) VCMI-Karten (*.vmap) - + Type Typ - + View surface Oberfläche anzeigen + + + No objects selected + + + + + This operation is irreversible. Do you want to continue? + + + + + Errors occured. %1 objects were not updated + + + + + Save to image + + MapSettings @@ -327,162 +370,220 @@ Karteneinstellungen - + General Allgemein - + Map name Kartenname - + Map description Kartenbeschreibung - + + Limit maximum heroes level + + + + Difficulty Schwierigkeit - + + Mods + + + + + Mandatory mods for playing this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + Events Ereignisse - + Victory Sieg - + Victory message Sieg-Nachricht - + Only for human players Nur für menschliche Spieler - + Allow standard victory Standardsieg zulassen - - + + Parameters Parameter - + Loss Niederlage - + 7 days without town 7 Tage ohne Stadt - + Defeat message Niederlage-Nachricht - + Abilities Fähigkeiten - + Spells Zaubersprüche - + Artifacts Artefakte - + Heroes Helden - + Ok Ok - + No special victory Kein besonderer Sieg - + Capture artifact Artefakt sammeln - + Hire creatures Kreaturen anheuern - + Accumulate resources Ressourcen ansammeln - + Construct building Gebäude errichten - + Capture town Stadt einnehmen - + Defeat hero Held besiegen - + Transport artifact Artefakt transportieren - + No special loss Keine besondere Niederlage - + Lose castle Schloss verlieren - + Lose hero Held verlieren - + Time expired Zeit abgelaufen - + Days without town Tage ohne Stadt + + MapView + + + Can't place object + + + MessageWidget @@ -494,47 +595,51 @@ PlayerParams - No team - Kein Team + Kein Team - + Human/CPU Mensch/CPU - + CPU only Nur CPU - + Team Team - + Main town Hauptstadt - + + Color + + + + Random faction Zufällige Fraktion - + Generate hero at main Held am Hauptplatz generieren - + (default) (Standard) - + Player ID: %1 Spieler-ID: %1 @@ -552,7 +657,12 @@ Spieler - + + 1 + 1 + + + Ok Ok @@ -608,6 +718,11 @@ Map is not loaded Karte ist nicht geladen + + + No factions allowed for player %1 + + No players allowed to play this map @@ -693,6 +808,11 @@ Map description is not specified Kartenbeschreibung ist nicht angegeben + + + Map contains object from mod "%1", but doesn't require it + + Exception occurs during validation: %1 @@ -762,114 +882,139 @@ Spieler - + 0 0 - + Human/Computer Mensch/Computer - - - - + + + + Random Zufall - + Computer only Nur Computer - + + Human teams + + + + + Computer teams + + + + Monster strength Monster-Stärke - + Weak Schwach - - + + Normal Normal - + Strong Stark - + Water content Wasseranteil - + None Keine - + Islands Inseln - + Template Vorlage - + Custom seed Benutzerdefiniertes Seed - + Generate random map Zufällige Karte generieren - + Ok Ok - + Cancel Abbrechen + + + No template + + + + + No template for parameters scecified. Random map cannot be generated. + + + + + RMG failure + + main - + Filepath of the map to open. Dateipfad der zu öffnenden Karte. - + Extract original H3 archives into a separate folder. Extrahieren Sie die Original-H3-Archive in einen separaten Ordner. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Aus einem extrahierten Archiv zerlegt es TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 und Un44 in einzelne PNGs. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Aus einem extrahierten Archiv werden einzelne Bilder (aus dem Ordner "Images") von .pcx in png konvertiert. - + Delete original files, for the ones splitted / converted. Löschen Sie die Originaldateien für die gesplitteten/konvertierten Dateien. diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index dcf038c89..c47231394 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -40,284 +40,327 @@ Plik - + Map Mapa - + Edit Edycja - + View Widok - + Player Gracz - + Toolbar Przybornik - + Minimap Minimapa - + Map Objects View Widok obiektów - + Browser Przeglądarka - + Inspector Inspektor - + Property Właściwość - + Value Wartość - + Terrains View Widok terenów - + Brush Pędzel - + Terrains Tereny - + Roads Drogi - + Rivers Rzeki - + Open Otwórz - + Save Zapisz - + New Nowy - + Save as... Zapisz jako - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Podziemia - - + + View underground Pokaż podziemia - + Pass Przejścia - + Cut Wytnij - + Copy Kopiuj - + Paste Wklej - + Fill Wypełnij - + Fills the selection with obstacles Wypełnia zaznaczony obszar przeszkodami - + Grid Siatka - + General Ogólne - + Map title and description Nazwa i opis mapy - + Players settings Ustawienia graczy - - + + Undo Cofnij - + Redo Przywróć - + Erase Wymaż - + Neutral Neutralny - + Validate Sprawdź - + + + + Update appearance Aktualizuj wygląd - + Recreate obstacles Powtórnie stwórz przeszkody - + Player 1 Gracz 1 - + Player 2 Gracz 2 - + Player 3 Gracz 3 - + Player 4 Gracz 4 - + Player 5 Gracz 5 - + Player 6 Gracz 6 - + Player 7 Gracz 7 - + Player 8 Gracz 8 - + Export as... - + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + + + + Failed to open map + + + + + Cannot open map from this folder + + + + Open map Otwórz mapę - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Wszystkie wspierane mapy (*.vmap *.h3m);;Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) - - + + Save map Zapisz mapę - - + + VCMI maps (*.vmap) Mapy VCMI (*.vmap) - + Type Typ - + View surface Pokaż powierzchnię + + + No objects selected + + + + + This operation is irreversible. Do you want to continue? + + + + + Errors occured. %1 objects were not updated + + + + + Save to image + + MapSettings @@ -327,162 +370,220 @@ Ustawienia mapy - + General Ogólne - + Map name Nazwa mapy - + Map description Opis mapy - + + Limit maximum heroes level + + + + Difficulty Poziom trudności - + + Mods + + + + + Mandatory mods for playing this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + Events Zdarzenia - + Victory Zwycięstwo - + Victory message Komunikat zwycięstwa - + Only for human players Dotyczy tylko graczy ludzkich - + Allow standard victory Także standardowy warunek zwycięstwa - - + + Parameters Parametry - + Loss Porażka - + 7 days without town 7 dni bez miasta - + Defeat message Komunikat o porażce - + Abilities Umiejętności - + Spells Zaklęcia - + Artifacts Artefakty - + Heroes Bohaterowie - + Ok Ok - + No special victory Bez specjalnych warunków zwycięstwa - + Capture artifact Zdobądź artefakt - + Hire creatures Zdobądź stworzenia - + Accumulate resources Zbierz zasoby - + Construct building Zbuduj budynek - + Capture town Zdobądź miasto - + Defeat hero Pokonaj bohatera - + Transport artifact Przenieś artefakt - + No special loss Bez specjalnych warunków porażki - + Lose castle Utrata miasta - + Lose hero Utrata bohatera - + Time expired Upłynięcie czasu - + Days without town Dni bez miasta + + MapView + + + Can't place object + + + MessageWidget @@ -494,47 +595,51 @@ PlayerParams - No team - Brak drużyny + Brak drużyny - + Human/CPU Człowiek/Komputer - + CPU only Tylko komputer - + Team Drużyna - + Main town Główne miasto - + + Color + + + + Random faction Losowe miasto - + Generate hero at main Generuj bohatera w głównym - + (default) (domyślny) - + Player ID: %1 ID gracza: %1 @@ -552,7 +657,12 @@ Gracze - + + 1 + 1 + + + Ok Ok @@ -608,6 +718,11 @@ Map is not loaded + + + No factions allowed for player %1 + + No players allowed to play this map @@ -693,6 +808,11 @@ Map description is not specified + + + Map contains object from mod "%1", but doesn't require it + + Exception occurs during validation: %1 @@ -762,114 +882,139 @@ Gracze - + 0 0 - + Human/Computer Człowiek/Komputer - - - - + + + + Random Losowo - + Computer only Tylko komputer - + + Human teams + + + + + Computer teams + + + + Monster strength Siła potworów - + Weak Słaba - - + + Normal Normalna - + Strong Silna - + Water content Powierzchnia wody - + None Brak - + Islands Wyspy - + Template Szablon - + Custom seed Własny seed - + Generate random map Generuj mapę losową - + Ok Ok - + Cancel Anuluj + + + No template + + + + + No template for parameters scecified. Random map cannot be generated. + + + + + RMG failure + + main - + Filepath of the map to open. Lokalizacja pliku mapy do otworzenia. - + Extract original H3 archives into a separate folder. Wyodrębnij oryginalne archiwa H3 do osobnego folderu. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Z wyodrębnionego archiwum, rozdzielenie TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 i Un44 do poszczególnych plików PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Z wyodrębnionego archiwum, konwersja pojedynczych obrazków (znalezionych w folderze Images) z .pcx do .png. - + Delete original files, for the ones splitted / converted. Usuń oryginalne pliki, dla już rozdzielonych / skonwertowanych. diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index 718573e2e..6c5641347 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -40,284 +40,327 @@ Файл - + Map Карта - + Edit Правка - + View Вид - + Player Игрок - + Toolbar Панель инструментов - + Minimap Мини-карта - + Map Objects View Объекты карты - + Browser Навигатор - + Inspector Инспектор - + Property Свойство - + Value Значение - + Terrains View Кисти земель - + Brush Кисть - + Terrains Земли - + Roads Дороги - + Rivers Реки - + Open Открыть - + Save Сохранить - + New Создать - + Save as... Сохранить как - + Ctrl+Shift+S Ctrl+Shift+S - + U/G П/Н - - + + View underground Вид на подземелье - + Pass Проходимость - + Cut Вырезать - + Copy Копировать - + Paste Вставить - + Fill Заливка - + Fills the selection with obstacles Заливает выбранное препятствиями - + Grid Сетка - + General Общее - + Map title and description Название и описание карты - + Players settings Настройки игроков - - + + Undo Отменить - + Redo Повторить - + Erase Удалить - + Neutral Нейтральный - + Validate Проверить - + + + + Update appearance Обновить вид - + Recreate obstacles Обновить препятствия - + Player 1 Игрок 1 - + Player 2 Игрок 2 - + Player 3 Игрок 3 - + Player 4 Игрок 4 - + Player 5 Игрок 5 - + Player 6 Игрок 6 - + Player 7 Игрок 7 - + Player 8 Игрок 8 - + Export as... - + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + + + + Failed to open map + + + + + Cannot open map from this folder + + + + Open map Открыть карту - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Все поддерживаемые карты (*.vmap *.h3m);;Карты VCMI (*.vmap);;Карты Героев III (*.h3m) - - + + Save map Сохранить карту - - + + VCMI maps (*.vmap) Карты VCMI (*.vmap) - + Type Тип - + View surface Вид на поверхность + + + No objects selected + + + + + This operation is irreversible. Do you want to continue? + + + + + Errors occured. %1 objects were not updated + + + + + Save to image + + MapSettings @@ -327,162 +370,220 @@ Настройки карты - + General Общее - + Map name Название карты - + Map description Описание карты - + + Limit maximum heroes level + + + + Difficulty Сложность - + + Mods + + + + + Mandatory mods for playing this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + Events События - + Victory Победа - + Victory message Сообщение о победе - + Only for human players Только для игроков-людей - + Allow standard victory Разрешить стандартную победу - - + + Parameters Параметры - + Loss Поражение - + 7 days without town 7 дней без городов - + Defeat message Сообщение о поражении - + Abilities Способности - + Spells Заклинания - + Artifacts Артефакты - + Heroes Герои - + Ok ОК - + No special victory Нет специальной победы - + Capture artifact Взять артефакт - + Hire creatures Нанять существ - + Accumulate resources Собрать ресурсы - + Construct building Построить - + Capture town Захватить город - + Defeat hero Победить героя - + Transport artifact Переместить артефакт - + No special loss Нет специального поражения - + Lose castle Потерять город - + Lose hero Потерять героя - + Time expired Не успеть ко времени - + Days without town Провести без городов + + MapView + + + Can't place object + + + MessageWidget @@ -494,47 +595,51 @@ PlayerParams - No team - Без команды + Без команды - + Human/CPU Человек/ИИ - + CPU only Только ИИ - + Team Команда - + Main town Главный город - + + Color + + + + Random faction Случайная фракция - + Generate hero at main Создать героя - + (default) (по умолчанию) - + Player ID: %1 Игрок: %1 @@ -552,7 +657,12 @@ Игрок - + + 1 + 1 + + + Ok ОК @@ -608,6 +718,11 @@ Map is not loaded + + + No factions allowed for player %1 + + No players allowed to play this map @@ -693,6 +808,11 @@ Map description is not specified + + + Map contains object from mod "%1", but doesn't require it + + Exception occurs during validation: %1 @@ -762,114 +882,139 @@ Игроки - + 0 0 - + Human/Computer Человек/ИИ - - - - + + + + Random Случайно - + Computer only Только ИИ - + + Human teams + + + + + Computer teams + + + + Monster strength Сила монстров - + Weak Слабо - - + + Normal Нормально - + Strong Сильно - + Water content Вода - + None Нет - + Islands Острова - + Template Шаблон - + Custom seed Пользовательское зерно - + Generate random map Сгенерировать случайную карту - + Ok ОК - + Cancel Отмена + + + No template + + + + + No template for parameters scecified. Random map cannot be generated. + + + + + RMG failure + + main - + Filepath of the map to open. Путь к файлу карты для открытия. - + Extract original H3 archives into a separate folder. Распаковать архивы оригинальных Героев III в отдельную папку. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Разделение в распакованном архиве TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 и Un44 на отдельные PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Преобразование в расспакованном архиве изображений .pcx в .png. - + Delete original files, for the ones splitted / converted. Удалить оригиналы для преобразованных файлов. diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 4d0ea87fc..73a9b60ed 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -27,8 +27,6 @@ Generando mapa - - MainWindow @@ -42,284 +40,327 @@ Archivo - + Map Mapa - + Edit Editar - + View Ver - + Player Jugador - + Toolbar Barra de herramientas - + Minimap Miniatura del mapa - + Map Objects View Vista de Objetos del Mapa - + Browser Navegador - + Inspector Inspector - + Property Propiedad - + Value Valor - + Terrains View Vista de Terrenos - + Brush Pincel - + Terrains Terrenos - + Roads Caminos - + Rivers Ríos - + Open Abrir - + Save Guardar - + New Nuevo - + Save as... Guardar como... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Subterráneo/Superficie - - + + View underground Ver subterráneo - + Pass Pasar - + Cut Cortar - + Copy Copiar - + Paste Pegar - + Fill Rellenar - + Fills the selection with obstacles Rellena la selección con obstáculos - + Grid Rejilla - + General General - + Map title and description Título y descripción del mapa - + Players settings Configuración de jugadores - - + + Undo Deshacer - + Redo Rehacer - + Erase Borrar - + Neutral Neutral - + Validate Validar - + + + + Update appearance Actualizar apariencia - + Recreate obstacles Recrear obstáculos - + Player 1 Jugador 1 - + Player 2 Jugador 2 - + Player 3 Jugador 3 - + Player 4 Jugador 4 - + Player 5 Jugador 5 - + Player 6 Jugador 6 - + Player 7 Jugador 7 - + Player 8 Jugador 8 - + Export as... Exportar como... - + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + + + + Failed to open map + + + + + Cannot open map from this folder + + + + Open map Abrir mapa - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Todos los mapas soportados (*.vmap *.h3m);;Mapas VCMI (*.vmap);;Mapas HoMM3 (*.h3m) - - + + Save map Guardar mapa - - + + VCMI maps (*.vmap) Mapas VCMI (*.vmap) - + Type Tipo - + View surface Ver superficie + + + No objects selected + + + + + This operation is irreversible. Do you want to continue? + + + + + Errors occured. %1 objects were not updated + + + + + Save to image + + MapSettings @@ -329,162 +370,220 @@ Configuración del mapa - + General General - + Map name Nombre del mapa - + Map description Descripción del mapa - + + Limit maximum heroes level + + + + Difficulty Dificultad - + + Mods + + + + + Mandatory mods for playing this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + Events Eventos - + Victory Victoria - + Victory message Mensaje de victoria - + Only for human players Solo para jugadores humanos - + Allow standard victory Permitir victoria estándar - - + + Parameters Parámetros - + Loss Derrota - + 7 days without town 7 días sin ciudad - + Defeat message Mensaje de derrota - + Abilities Habilidades - + Spells Hechizos - + Artifacts Artefactos - + Heroes Héroes - + Ok Aceptar - + No special victory Sin victoria especial - + Capture artifact Capturar artefacto - + Hire creatures Contratar criaturas - + Accumulate resources Acumular recursos - + Construct building Construir edificio - + Capture town Capturar ciudad - + Defeat hero Vencer héroe - + Transport artifact Transportar artefacto - + No special loss Sin pérdida especial - + Lose castle Perder castillo - + Lose hero Perder héroe - + Time expired Expiró el tiempo - + Days without town Días sin ciudad + + MapView + + + Can't place object + + + MessageWidget @@ -496,47 +595,51 @@ PlayerParams - No team - Sin equipo + Sin equipo - + Human/CPU Humano/CPU - + CPU only Sólo CPU - + Team Equipo - + Main town Ciudad principal - + + Color + + + + Random faction Facción aleatoria - + Generate hero at main Generar héroe en la ciudad principal - + (default) (predeterminado) - + Player ID: %1 ID de jugador: %1 @@ -554,7 +657,12 @@ Jugadores - + + 1 + 1 + + + Ok Aceptar @@ -610,6 +718,11 @@ Map is not loaded No se ha cargado ningún mapa + + + No factions allowed for player %1 + + No players allowed to play this map @@ -695,6 +808,11 @@ Map description is not specified No se especifica la descripción del mapa + + + Map contains object from mod "%1", but doesn't require it + + Exception occurs during validation: %1 @@ -764,114 +882,139 @@ Jugadores - + 0 0 - + Human/Computer Humano/Ordenador - - - - + + + + Random Aleatorio - + Computer only Sólo ordenador - + + Human teams + + + + + Computer teams + + + + Monster strength Fuerza de monstruos - + Weak Débil - - + + Normal Normal - + Strong Fuerte - + Water content Contenido del agua - + None Ninguno - + Islands Islas - + Template Plantilla - + Custom seed Semilla personalizada - + Generate random map Generar un mapa aleatorio - + Ok Aceptar - + Cancel Cancelar + + + No template + + + + + No template for parameters scecified. Random map cannot be generated. + + + + + RMG failure + + main - + Filepath of the map to open. Ruta del archivo del mapa a abrir. - + Extract original H3 archives into a separate folder. Extraer archivos originales de H3 en una carpeta separada. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Desde un archivo extraído, separa TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 y Un44 en imágenes PNG individuales. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Desde un archivo extraído, convierte imágenes individuales (encontradas en la carpeta Imágenes) de .pcx a png. - + Delete original files, for the ones splitted / converted. Eliminar archivos originales, por los que se han separado / convertido. diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index d823173da..8470bca90 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -40,284 +40,327 @@ Файл - + Map Мапа - + Edit Редагування - + View Вигляд - + Player Гравець - + Toolbar Панель інструментів - + Minimap Мінімапа - + Map Objects View Перегляд об'єктів мапи - + Browser Навігатор - + Inspector Інспектор - + Property Властивість - + Value Значення - + Terrains View Перегляд поверхні - + Brush Кисть - + Terrains Землі - + Roads Шляхи - + Rivers Річки - + Open Відкрити - + Save Зберегти - + New Створити - + Save as... - Зберегти як + Зберегти як... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G П/З - - + + View underground Дивитись підземелля - + Pass Прохідність - + Cut Вирізати - + Copy Скопіювати - + Paste Вставити - + Fill Заповнити - + Fills the selection with obstacles Заповнити перешкодами - + Grid Сітка - + General Загальний - + Map title and description Назва та опис мапи - + Players settings Налаштування гравців - - + + Undo Відмінити - + Redo Повторити - + Erase Стерти - + Neutral Нейтральний - + Validate Перевірити - + + + + Update appearance Оновити вигляд - + Recreate obstacles Оновити перешкоди - + Player 1 Гравець 1 - + Player 2 Гравець 2 - + Player 3 Гравець 3 - + Player 4 Гравець 4 - + Player 5 Гравець 5 - + Player 6 Гравець 6 - + Player 7 Гравець 7 - + Player 8 Гравець 8 - + Export as... + Експортувати як... + + + + Confirmation - + + Unsaved changes will be lost, are you sure? + + + + + Failed to open map + + + + + Cannot open map from this folder + + + + Open map Відкрити мапу - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Всі підтримувані мапи (*.vmap *.h3m);;Мапи VCMI (*.vmap);;Мапи HoMM3 (*.h3m) - - + + Save map Зберегти мапу - - + + VCMI maps (*.vmap) Мапи VCMI - + Type Тип - + View surface Дивитись поверхню + + + No objects selected + + + + + This operation is irreversible. Do you want to continue? + + + + + Errors occured. %1 objects were not updated + + + + + Save to image + + MapSettings @@ -327,162 +370,220 @@ Налаштування мапи - + General Загальний - + Map name Назва мапи - + Map description Опис мапи - + + Limit maximum heroes level + Обмежити максимальний рівень героїв + + + Difficulty Складність - + + Mods + Модифікації + + + + Mandatory mods for playing this map + Модифікації необхідні для гри на мапи + + + + Mod name + Назва модифікації + + + + Version + Версія + + + + Automatic assignment + Автоматичне визначення + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Встановити необхідні модифікації на основі об'єктів, розміщених на мапі. Цей метод може викликати проблеми, якщо у вас є налаштовані нагороди, гарнізони тощо з модів + + + + Map objects mods + Моди з об'єктами мапи + + + + Set all mods having a game content as mandatory + Встановити усі моди з ігровим контентом як обов'язкові + + + + Full content mods + Усі модифікації + + + Events Події - + Victory Перемога - + Victory message Повідомлення про перемогу - + Only for human players Тільки для гравців-людей - + Allow standard victory Дозволити типову перемогу - - + + Parameters Параметри - + Loss Програш - + 7 days without town 7 днів без міста - + Defeat message Повідомлення про програш - + Abilities Уміння - + Spells Закляття - + Artifacts Артефакти - + Heroes Герої - + Ok Підтвердити - + No special victory Немає особливої перемоги - + Capture artifact Отримати артефакт - + Hire creatures Найняти істот - + Accumulate resources Накопичити ресурси - + Construct building Побудувати будівлю - + Capture town Захопити місто - + Defeat hero Перемогти героя - + Transport artifact Доставити артефакт - + No special loss Немає особливої поразки - + Lose castle Втратити місто - + Lose hero Втратити героя - + Time expired Закінчився час - + Days without town Дні без міста + + MapView + + + Can't place object + + + MessageWidget @@ -494,47 +595,47 @@ PlayerParams - - No team - Без команди - - - + Human/CPU Людина/Комп'ютер - + CPU only Тільки комп'ютер - + Team Команда - + Main town Головне місто - + + Color + Колір + + + Random faction Випадкова фракція - + Generate hero at main Згенерувати героя - + (default) (за замовчуванням) - + Player ID: %1 Гравець %1 @@ -552,7 +653,12 @@ Гравці - + + 1 + 1 + + + Ok Підтвердити @@ -608,6 +714,11 @@ Map is not loaded + + + No factions allowed for player %1 + + No players allowed to play this map @@ -693,6 +804,11 @@ Map description is not specified + + + Map contains object from mod "%1", but doesn't require it + + Exception occurs during validation: %1 @@ -762,114 +878,139 @@ Гравців - + 0 0 - + Human/Computer Людина/Комп'ютер - - - - + + + + Random Випадково - + Computer only Тільки комп'ютер - + + Human teams + Команди людей + + + + Computer teams + Команди комп'ютерів + + + Monster strength Сила монстрів - + Weak Слабкі - - + + Normal Типова - + Strong Сильні - + Water content Наявність води - + None Відсутня - + Islands Острови - + Template Шаблон - + Custom seed Користувацьке зерно - + Generate random map Згенерувати випадкову карту - + Ok Підтвердити - + Cancel Скасувати + + + No template + + + + + No template for parameters scecified. Random map cannot be generated. + + + + + RMG failure + + main - + Filepath of the map to open. - + Extract original H3 archives into a separate folder. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. - + Delete original files, for the ones splitted / converted. From 8e1dc785eb87c49775b1a963239d2504dc26afd7 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Fri, 28 Jul 2023 14:19:12 +0300 Subject: [PATCH 055/168] Wrong player was engaged for 5lvl dwelling --- server/CGameHandler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 9df39108a..0d2f16456 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2118,6 +2118,9 @@ void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const //send info about battles BattleStart bs; bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); + + engageIntoBattle(bs.info->sides[0].color); + engageIntoBattle(bs.info->sides[1].color); sendAndApply(&bs); } @@ -2577,9 +2580,6 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI if(gs->curB) gs->curB.dellNull(); - engageIntoBattle(army1->tempOwner); - engageIntoBattle(army2->tempOwner); - static const CArmedInstance *armies[2]; armies[0] = army1; armies[1] = army2; From aaa115cf4b70e7fb5a964a062af765f0e62aaaf0 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 29 Jul 2023 00:11:16 +0200 Subject: [PATCH 056/168] Button-Sound -> haptic feedback --- client/widgets/Buttons.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index 1bfdf5908..ae4737df7 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -18,6 +18,7 @@ #include "../CPlayerInterface.h" #include "../battle/BattleInterface.h" #include "../battle/BattleInterfaceClasses.h" +#include "../eventsSDL/InputHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/MouseButton.h" #include "../gui/Shortcut.h" @@ -175,7 +176,10 @@ void CButton::clickPressed(const Point & cursorPosition) if (getState() != PRESSED) { if (!soundDisabled) + { CCS->soundh->playSound(soundBase::button); + GH.input().hapticFeedback(); + } setState(PRESSED); if (actOnDown) @@ -403,6 +407,7 @@ void CToggleButton::clickPressed(const Point & cursorPosition) if (canActivate()) { CCS->soundh->playSound(soundBase::button); + GH.input().hapticFeedback(); setState(PRESSED); } } From a73146751ca12f6a95f505f1f3ca05abe95f8f48 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 30 Jul 2023 12:30:56 +0300 Subject: [PATCH 057/168] Execute cursor changes in main thread on any platform --- client/renderSDL/CursorHardware.cpp | 33 +++++++++++------------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/client/renderSDL/CursorHardware.cpp b/client/renderSDL/CursorHardware.cpp index 1161ee176..d68ed5f62 100644 --- a/client/renderSDL/CursorHardware.cpp +++ b/client/renderSDL/CursorHardware.cpp @@ -11,6 +11,7 @@ #include "StdInc.h" #include "CursorHardware.h" +#include "../gui/CGuiHandler.h" #include "../render/Colors.h" #include "../render/IImage.h" #include "SDL_Extensions.h" @@ -18,10 +19,6 @@ #include #include -#ifdef VCMI_APPLE -#include -#endif - CursorHardware::CursorHardware(): cursor(nullptr) { @@ -36,16 +33,13 @@ CursorHardware::~CursorHardware() void CursorHardware::setVisible(bool on) { -#ifdef VCMI_APPLE - dispatch_async(dispatch_get_main_queue(), ^{ -#endif - if (on) - SDL_ShowCursor(SDL_ENABLE); - else - SDL_ShowCursor(SDL_DISABLE); -#ifdef VCMI_APPLE + GH.dispatchMainThread([on]() + { + if (on) + SDL_ShowCursor(SDL_ENABLE); + else + SDL_ShowCursor(SDL_DISABLE); }); -#endif } void CursorHardware::setImage(std::shared_ptr image, const Point & pivotOffset) @@ -63,16 +57,13 @@ void CursorHardware::setImage(std::shared_ptr image, const Point & pivot logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError()); SDL_FreeSurface(cursorSurface); -#ifdef VCMI_APPLE - dispatch_async(dispatch_get_main_queue(), ^{ -#endif - SDL_SetCursor(cursor); - if (oldCursor) - SDL_FreeCursor(oldCursor); -#ifdef VCMI_APPLE + GH.dispatchMainThread([this, oldCursor](){ + SDL_SetCursor(cursor); + + if (oldCursor) + SDL_FreeCursor(oldCursor); }); -#endif } void CursorHardware::setCursorPosition( const Point & newPos ) From 5776e171c4212fe18a259f9575df7d3929698a01 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 30 Jul 2023 12:31:35 +0300 Subject: [PATCH 058/168] Fix list of possible resources given by some objects, e.g. windmills --- config/objects/rewardableOncePerWeek.json | 2 +- config/objects/rewardableOnceVisitable.json | 4 ++-- config/objects/rewardablePickable.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/objects/rewardableOncePerWeek.json b/config/objects/rewardableOncePerWeek.json index 464f8ff32..e83375b8c 100644 --- a/config/objects/rewardableOncePerWeek.json +++ b/config/objects/rewardableOncePerWeek.json @@ -151,7 +151,7 @@ "message" : 170, "resources" : [ { - "list" : [ "ore", "mercury", "gems", "sulfur", "crystal" ], + "anyOf" : [ "ore", "mercury", "gems", "sulfur", "crystal" ], "min" : 3, "max" : 6 } diff --git a/config/objects/rewardableOnceVisitable.json b/config/objects/rewardableOnceVisitable.json index 3eab395fc..353b2d666 100644 --- a/config/objects/rewardableOnceVisitable.json +++ b/config/objects/rewardableOnceVisitable.json @@ -27,7 +27,7 @@ "message" : 64, "resources" : [ { - "list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ], + "anyOf" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ], "min" : 1, "max" : 5 } @@ -115,7 +115,7 @@ "appearChance" : { "min" : 40, "max" : 90 }, "resources" : [ { - "list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ], + "anyOf" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ], "min" : 2, "max" : 5 }, diff --git a/config/objects/rewardablePickable.json b/config/objects/rewardablePickable.json index 2f4b64ef9..b5756a528 100644 --- a/config/objects/rewardablePickable.json +++ b/config/objects/rewardablePickable.json @@ -30,7 +30,7 @@ "removeObject" : true, "resources" : [ { - "list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ], + "anyOf" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ], "min" : 4, "max" : 6 }, From d86c0a969d32313827d2a686cd7b831d1fc8d6bc Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 30 Jul 2023 12:34:34 +0300 Subject: [PATCH 059/168] Fix conflicting market/town hotkeys --- client/gui/ShortcutHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/gui/ShortcutHandler.cpp b/client/gui/ShortcutHandler.cpp index a36a1ddbc..9014d1fcf 100644 --- a/client/gui/ShortcutHandler.cpp +++ b/client/gui/ShortcutHandler.cpp @@ -76,7 +76,7 @@ std::vector ShortcutHandler::translateKeycode(SDL_Keycode key) const {SDLK_r, EShortcut::GAME_RESTART_GAME }, {SDLK_m, EShortcut::GAME_TO_MAIN_MENU }, {SDLK_q, EShortcut::GAME_QUIT_GAME }, - {SDLK_t, EShortcut::GAME_OPEN_MARKETPLACE }, + {SDLK_b, EShortcut::GAME_OPEN_MARKETPLACE }, {SDLK_g, EShortcut::GAME_OPEN_THIEVES_GUILD }, {SDLK_TAB, EShortcut::GAME_ACTIVATE_CONSOLE }, {SDLK_o, EShortcut::ADVENTURE_GAME_OPTIONS }, From 95dd4b3ffbd6a112ca934474f15f8ad2a82d03ed Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 30 Jul 2023 13:19:52 +0300 Subject: [PATCH 060/168] Fix incorrect warnings about missing translations --- lib/CGeneralTextHandler.cpp | 12 ++++++++++++ lib/CGeneralTextHandler.h | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index d949f1841..eb2cd8900 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -313,6 +313,18 @@ void CGeneralTextHandler::registerStringOverride(const std::string & modContext, assert(!modContext.empty()); assert(!language.empty()); + std::string baseModLanguage = getModLanguage(modContext); + + if (baseModLanguage != language) + { + // this is translation - only add text to existing strings, do not register new ones + if (stringsLocalizations.count(UID.get()) == 0) + { + logMod->warn("Unknown string '%s' in mod '%s' for language '%s'. Ignoring", UID.get(), modContext, language); + return; + } + } + // NOTE: implicitly creates entry, intended - strings added by vcmi (and potential UI mods) are not registered anywhere at the moment auto & entry = stringsLocalizations[UID.get()]; diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index daf3ba94b..83a7ee7b4 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -141,8 +141,11 @@ class DLL_LINKAGE CGeneralTextHandler std::vector scenariosCountPerCampaign; std::string getModLanguage(const std::string & modContext); -public: + /// add selected string to internal storage as high-priority strings + void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized); + +public: /// validates translation of specified language for specified mod /// returns true if localization is valid and complete /// any error messages will be written to log file @@ -155,9 +158,6 @@ public: /// add selected string to internal storage void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized); - /// add selected string to internal storage as high-priority strings - void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized); - // returns true if identifier with such name was registered, even if not translated to current language // not required right now, can be added if necessary // bool identifierExists( const std::string identifier) const; From 397c4c75952c208b6993f628f855c88998b2a04d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 30 Jul 2023 15:28:56 +0300 Subject: [PATCH 061/168] Fix dismissing heroes on map with "Capture town" victory condition --- lib/mapObjects/CGHeroInstance.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 36ac4e7ea..5bacb0c73 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1718,22 +1718,25 @@ bool CGHeroInstance::isMissionCritical() const { for(const TriggeredEvent & event : IObjectInterface::cb->getMapHeader()->triggeredEvents) { - if(event.trigger.test([&](const EventCondition & condition) + if (event.effect.type != EventEffect::DEFEAT) + continue; + + auto const & testFunctor = [&](const EventCondition & condition) { if ((condition.condition == EventCondition::CONTROL || condition.condition == EventCondition::HAVE_0) && condition.object) { const auto * hero = dynamic_cast(condition.object); return (hero != this); } - else if(condition.condition == EventCondition::IS_HUMAN) - { + + if(condition.condition == EventCondition::IS_HUMAN) return true; - } + return false; - })) - { + }; + + if(event.trigger.test(testFunctor)) return true; - } } return false; } From 5d2dd4c633b82eee2d514965d7198a1786c18551 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 30 Jul 2023 15:29:18 +0300 Subject: [PATCH 062/168] Show creature popup window on right-click in town fort --- client/windows/CCastleInterface.cpp | 9 +++++++++ client/windows/CCastleInterface.h | 2 ++ 2 files changed, 11 insertions(+) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index e3f743aec..dda745cea 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -15,6 +15,7 @@ #include "InfoWindows.h" #include "GUIClasses.h" #include "QuickRecruitmentWindow.h" +#include "CCreatureWindow.h" #include "../CGameInfo.h" #include "../CMusicHandler.h" @@ -1653,6 +1654,8 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * if(!town->creatures[level].second.empty()) addUsedEvents(LCLICK | HOVER);//Activate only if dwelling is present + addUsedEvents(SHOW_POPUP); + icons = std::make_shared("TPCAINFO", 261, 3); if(getMyBuilding() != nullptr) @@ -1740,6 +1743,12 @@ void CFortScreen::RecruitArea::clickPressed(const Point & cursorPosition) LOCPLINT->castleInt->builds->enterDwelling(level); } +void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition) +{ + if (getMyCreature() != nullptr) + GH.windows().createAndPushWindow(getMyCreature(), true); +} + CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem) : CStatusbarWindow(BORDERED, imagem) { diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 05ea80fe1..d3bbc29ae 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -347,6 +347,8 @@ class CFortScreen : public CStatusbarWindow void creaturesChangedEventHandler(); void hover(bool on) override; void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + }; std::shared_ptr title; std::vector> recAreas; From a7fab1099c87be3669aaf07e1d0c244bff3ae9c3 Mon Sep 17 00:00:00 2001 From: heroesiiifan <77574150+heroesiiifan@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:10:38 +0000 Subject: [PATCH 063/168] Mapeditor translation german update --- mapeditor/translation/german.ts | 56 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 61ca61ce6..4da2aee66 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -292,22 +292,22 @@ Confirmation - + Bestätigung Unsaved changes will be lost, are you sure? - + Ungespeicherte Änderungen gehen verloren, sind sie sicher? Failed to open map - + Öffnen der Karte fehlgeschlagen Cannot open map from this folder - + Kann keine Karte aus diesem Ordner öffnen @@ -344,22 +344,22 @@ No objects selected - + Keine Objekte selektiert This operation is irreversible. Do you want to continue? - + Diese Operation ist unumkehrbar. Möchten sie fortsetzen? Errors occured. %1 objects were not updated - + Fehler sind aufgetreten. %1 Objekte konnten nicht aktualisiert werden Save to image - + Als Bild speichern @@ -387,7 +387,7 @@ Limit maximum heroes level - + Maximales Level des Helden begrenzen @@ -397,47 +397,47 @@ Mods - + Mods Mandatory mods for playing this map - + Notwendige Mods zum Spielen dieser Karte Mod name - + Mod Name Version - + Version Automatic assignment - + Automatische Zuweisung Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - + Erforderliche Mods anhand der auf der Karte platzierten Objekte festlegen. Diese Methode kann Probleme verursachen, wenn Sie Belohnungen, Garnisonen usw. von Mods angepasst haben Map objects mods - + Mods für Kartenobjekte Set all mods having a game content as mandatory - + Alle Mods, die einen Spielinhalt haben, als notwendig festlegen Full content mods - + Vollwertige Mods @@ -581,7 +581,7 @@ Can't place object - + Objekt kann nicht platziert werden @@ -621,7 +621,7 @@ Color - + Farbe @@ -659,7 +659,7 @@ 1 - 1 + 1 @@ -721,7 +721,7 @@ No factions allowed for player %1 - + Keine Fraktionen für Spieler %1 erlaubt @@ -811,7 +811,7 @@ Map contains object from mod "%1", but doesn't require it - + Karte enthält Objekt aus Mod "%1", benötigt es aber nicht @@ -907,12 +907,12 @@ Human teams - + Menschliche Teams Computer teams - + Computer Teams @@ -978,17 +978,17 @@ No template - + Kein Template No template for parameters scecified. Random map cannot be generated. - + Es wurde kein Template für Parameter erstellt. Zufällige Karte kann nicht generiert werden. RMG failure - + RMG-Fehler From e9b46363ccbd3cfc89a553e19cc4016782bc6d9b Mon Sep 17 00:00:00 2001 From: heroesiiifan <77574150+heroesiiifan@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:11:56 +0000 Subject: [PATCH 064/168] Launcher translation german update --- launcher/translation/german.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index b28163a84..2840fb21f 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -472,7 +472,7 @@ Autosave limit (0 = off) - + Limit für Autospeicherung (0 = aus) @@ -487,12 +487,12 @@ Autosave prefix - + Präfix für Autospeicherung empty = map name prefix - + leer = Kartenname als Präfix @@ -911,7 +911,7 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Vietnamese - + Vietnamesisch From 986c8799a843ba2ef28412981b3fb7aa748dbede Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 31 Jul 2023 13:31:44 +0300 Subject: [PATCH 065/168] Move checkboxes 2 slots above to avoid overflowing below window border --- config/widgets/settings/adventureOptionsTab.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/widgets/settings/adventureOptionsTab.json b/config/widgets/settings/adventureOptionsTab.json index 49842039b..cee89b6eb 100644 --- a/config/widgets/settings/adventureOptionsTab.json +++ b/config/widgets/settings/adventureOptionsTab.json @@ -7,7 +7,7 @@ "name": "lineLabelsEnd", "type": "texture", "image": "settingsWindow/lineHorizontal", - "rect": { "x" : 5, "y" : 289, "w": 365, "h": 3} + "rect": { "x" : 5, "y" : 229, "w": 365, "h": 3} }, /////////////////////////////////////// Left section - Hero Speed and Map Scrolling { @@ -323,7 +323,7 @@ { "type": "verticalLayout", "customType": "labelDescription", - "position": {"x": 45, "y": 295}, + "position": {"x": 45, "y": 235}, "items": [ { @@ -353,7 +353,7 @@ { "type": "verticalLayout", "customType": "checkbox", - "position": {"x": 10, "y": 293}, + "position": {"x": 10, "y": 233}, "items": [ { From 38a98387e490be61d3d11a4870758241d87a6b68 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 13 May 2023 10:25:40 +0300 Subject: [PATCH 066/168] Temp fix for blocked in garrison ai. --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index c0d650464..2fbd50a72 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,7 +879,7 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison) + if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first)) continue; uint64_t mask = FirstActorMask << actors.size(); From b1ca663eb61e110a7cc048fa3e017d04922da7eb Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 14 May 2023 09:17:15 +0300 Subject: [PATCH 067/168] Fuzzy rework --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 69 +++++++++++- AI/Nullkiller/Engine/PriorityEvaluator.h | 3 + AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- config/ai/object-priorities.txt | 113 ++++---------------- 4 files changed, 93 insertions(+), 94 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 05be5a68c..eabd52403 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -49,7 +49,8 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) turn(0), strategicalValue(0), evaluator(ai), - enemyHeroDangerRatio(0) + enemyHeroDangerRatio(0), + armyGrowth(0) { } @@ -64,6 +65,7 @@ void PriorityEvaluator::initVisitTile() std::string str = std::string((char *)file.first.get(), file.second); engine = fl::FllImporter().fromString(str); armyLossPersentageVariable = engine->getInputVariable("armyLoss"); + armyGrowthVariable = engine->getInputVariable("armyGrowth"); heroRoleVariable = engine->getInputVariable("heroRole"); dangerVariable = engine->getInputVariable("danger"); turnVariable = engine->getInputVariable("turn"); @@ -164,7 +166,7 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero return result; } -uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool checkGold) +uint64_t getDwellingArmyValue(CCallback * cb, const CGObjectInstance * target, bool checkGold) { auto dwelling = dynamic_cast(target); uint64_t score = 0; @@ -185,6 +187,27 @@ uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool return score; } +uint64_t getDwellingArmyGrowth(CCallback * cb, const CGObjectInstance * target, PlayerColor myColor) +{ + auto dwelling = dynamic_cast(target); + uint64_t score = 0; + + if(dwelling->getOwner() == myColor) + return 0; + + for(auto & creLevel : dwelling->creatures) + { + if(creLevel.second.size()) + { + auto creature = creLevel.second.back().toCreature(); + + score += creature->getAIValue() * creature->getGrowth(); + } + } + + return score; +} + int getDwellingArmyCost(const CGObjectInstance * target) { auto dwelling = dynamic_cast(target); @@ -272,7 +295,7 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR4: - return getDwellingScore(ai->cb.get(), target, checkGold); + return getDwellingArmyValue(ai->cb.get(), target, checkGold); case Obj::CRYPT: case Obj::SHIPWRECK: case Obj::SHIPWRECK_SURVIVOR: @@ -293,6 +316,44 @@ uint64_t RewardEvaluator::getArmyReward( } } +uint64_t RewardEvaluator::getArmyGrowth( + const CGObjectInstance * target, + const CGHeroInstance * hero, + const CCreatureSet * army) const +{ + const float enemyArmyEliminationRewardRatio = 0.5f; + + if(!target) + return 0; + + switch(target->ID) + { + case Obj::TOWN: + { + auto town = dynamic_cast(target); + auto fortLevel = town->fortLevel(); + auto neutral = !town->getOwner().isValidPlayer(); + auto booster = isAnotherAi(town, *ai->cb) || neutral ? 1 : 2; + + if(fortLevel < CGTownInstance::CITADEL) + return town->hasFort() ? booster * 500 : 0; + else + return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000); + } + + case Obj::CREATURE_GENERATOR1: + case Obj::CREATURE_GENERATOR2: + case Obj::CREATURE_GENERATOR3: + case Obj::CREATURE_GENERATOR4: + return getDwellingArmyGrowth(ai->cb.get(), target, hero->getOwner()); + case Obj::ARTIFACT: + // it is not supported now because hero will not sit in town on 7th day but later parts of legion may be counted as army growth as well. + return 0; + default: + return 0; + } +} + int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const { if(!target) @@ -714,6 +775,7 @@ public: { evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); + evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole); evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); @@ -920,6 +982,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); goldRewardVariable->setValue(evaluationContext.goldReward); armyRewardVariable->setValue(evaluationContext.armyReward); + armyGrowthVariable->setValue(evaluationContext.armyGrowth); skillRewardVariable->setValue(evaluationContext.skillReward); dangerVariable->setValue(evaluationContext.danger); rewardTypeVariable->setValue(rewardType); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 840169970..a07ebf3f6 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -33,6 +33,7 @@ public: RewardEvaluator(const Nullkiller * ai) : ai(ai) {} uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const; + uint64_t getArmyGrowth(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const; float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const; float getResourceRequirementStrength(int resType) const; @@ -54,6 +55,7 @@ struct DLL_EXPORT EvaluationContext float closestWayRatio; float armyLossPersentage; float armyReward; + uint64_t armyGrowth; int32_t goldReward; int32_t goldCost; float skillReward; @@ -95,6 +97,7 @@ private: fl::InputVariable * turnVariable; fl::InputVariable * goldRewardVariable; fl::InputVariable * armyRewardVariable; + fl::InputVariable * armyGrowthVariable; fl::InputVariable * dangerVariable; fl::InputVariable * skillRewardVariable; fl::InputVariable * strategicalValueVariable; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 2fbd50a72..c0d650464 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,7 +879,7 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first)) + if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison) continue; uint64_t mask = FirstActorMask << actors.size(); diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index b90a350f7..1714c8477 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -121,106 +121,39 @@ InputVariable: fear term: LOW Triangle 0.000 0.500 1.000 term: MEDIUM Triangle 0.500 1.000 1.500 term: HIGH Ramp 1.000 1.800 +InputVariable: armyGrowth + enabled: true + range: 0.000 20000.000 + lock-range: false + term: NONE Ramp 100.000 0.000 + term: SMALL Triangle 0.000 1000.000 3000.000 + term: MEDIUM Triangle 1000.000 3000.000 8000.000 + term: BIG Triangle 3000.000 8000.000 20000.000 + term: HUGE Ramp 8000.000 20000.000 OutputVariable: Value enabled: true - range: -0.500 1.500 + range: -1.500 2.000 lock-range: false aggregation: AlgebraicSum defuzzifier: Centroid 100 default: 0.500 lock-previous: false - term: LOWEST Discrete -0.500 0.000 -0.500 1.000 -0.200 1.000 -0.200 0.000 0.200 0.000 0.200 1.000 0.500 1.000 0.500 0.000 0.500 - term: BITLOW Rectangle -0.010 0.010 0.500 - term: LOW Discrete -0.150 0.000 -0.150 1.000 -0.050 1.000 -0.050 0.000 0.050 0.000 0.050 1.000 0.150 1.000 0.150 0.000 0.500 - term: MEDIUM Triangle 0.450 0.500 0.550 0.050 - term: HIGH Discrete 0.850 0.000 0.850 1.000 0.950 1.000 0.950 0.000 1.050 0.000 1.050 1.000 1.150 1.000 1.150 0.000 0.500 - term: HIGHEST Discrete 0.500 0.000 0.500 1.000 0.800 1.000 0.800 0.000 1.200 0.000 1.200 1.000 1.500 1.000 1.500 0.000 0.500 - term: BITHIGH Rectangle 0.990 1.010 0.500 + term: WORST Binary -1.000 -inf 0.500 + term: BAD Rectangle -1.000 -0.700 0.500 + term: BASE Rectangle -0.200 0.200 0.400 + term: LOW Rectangle 1.110 1.190 0.320 + term: HIGHEST Discrete 0.300 0.000 0.300 1.000 0.600 1.000 0.600 0.000 1.700 0.000 1.700 1.000 2.000 1.000 2.000 0.000 0.500 + term: HIGH Discrete 0.600 0.000 0.600 1.000 0.850 1.000 0.850 0.000 1.450 0.000 1.450 1.000 1.700 1.000 1.700 0.000 0.400 + term: BITHIGH Discrete 0.850 0.000 0.850 1.000 1.000 1.000 1.000 0.000 1.300 0.000 1.300 1.000 1.450 1.000 1.450 0.000 0.350 + term: MEDIUM Discrete 1.000 0.000 1.000 1.000 1.100 1.000 1.100 0.000 1.200 0.000 1.200 1.000 1.300 1.000 1.300 0.000 0.330 RuleBlock: gold reward enabled: true conjunction: AlgebraicProduct disjunction: AlgebraicSum implication: AlgebraicProduct activation: General - rule: if turn is NOW and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.5 - rule: if turn is NOW and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3 - rule: if turn is NOW and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3 - rule: if turn is NOW and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3 - rule: if turn is NEXT and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.8 - rule: if turn is NEXT and scoutTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW - rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3 - rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW with 0.3 - rule: if turn is FUTURE and scoutTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.3 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is NONE then Value is LOWEST with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is LOW then Value is LOWEST with 0.3 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is MEDIUM then Value is LOW with 0.5 - rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is HIGH then Value is BITLOW - rule: if turn is FUTURE and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW - rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW - rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW - rule: if scoutTurnDistance is MEDIUM and heroRole is SCOUT then Value is BITLOW - rule: if mainTurnDistance is MEDIUM then Value is BITLOW - rule: if scoutTurnDistance is LOW and heroRole is SCOUT then Value is MEDIUM - rule: if mainTurnDistance is LOW then Value is MEDIUM - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH with 0.7 - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is BITHIGH - rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH - rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is SCOUT and danger is not NONE then Value is MEDIUM - rule: if goldReward is MEDIUM and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is BITLOW - rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is MAIN and danger is not NONE then Value is BITHIGH - rule: if goldReward is LOW and goldPreasure is HIGH and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH - rule: if goldReward is LOW and heroRole is MAIN and danger is not NONE and rewardType is SINGLE and armyLoss is LOW then Value is BITLOW - rule: if goldReward is LOW and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOW - rule: if goldReward is LOWEST and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOWEST - rule: if armyReward is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is HIGH with 0.5 - rule: if armyReward is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and mainTurnDistance is LOWEST then Value is HIGHEST - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is NONE and fear is not HIGH then Value is HIGH - rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST with 0.5 - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is NONE then Value is BITHIGH - rule: if armyReward is MEDIUM and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH with 0.2 - rule: if armyReward is MEDIUM and heroRole is SCOUT and danger is NONE then Value is HIGHEST with 0.5 - rule: if armyReward is LOW and heroRole is SCOUT and danger is NONE then Value is HIGH - rule: if armyReward is LOW and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH - rule: if armyReward is LOW and heroRole is MAIN and danger is NONE then Value is BITLOW with 0.5 - rule: if armyReward is LOW and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH - rule: if skillReward is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH - rule: if skillReward is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is BITHIGH - rule: if skillReward is MEDIUM and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5 - rule: if skillReward is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if skillReward is MEDIUM and heroRole is SCOUT then Value is LOWEST - rule: if skillReward is HIGH and heroRole is SCOUT then Value is LOWEST - rule: if strategicalValue is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH - rule: if strategicalValue is LOWEST and heroRole is MAIN and armyLoss is LOW then Value is LOW - rule: if strategicalValue is LOW and heroRole is SCOUT and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5 - rule: if strategicalValue is MEDIUM and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGH - rule: if strategicalValue is HIGH and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGHEST with 0.5 - rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is MEDIUM and fear is not HIGH then Value is HIGH - rule: if strategicalValue is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if rewardType is NONE then Value is LOWEST - rule: if armyLoss is HIGH and strategicalValue is not HIGH and heroRole is MAIN then Value is LOWEST - rule: if armyLoss is HIGH and strategicalValue is HIGH and heroRole is MAIN then Value is LOW - rule: if armyLoss is HIGH and heroRole is SCOUT then Value is LOWEST - rule: if heroRole is SCOUT and closestHeroRatio is LOW then Value is LOW - rule: if heroRole is SCOUT and closestHeroRatio is LOWEST then Value is LOWEST - rule: if heroRole is MAIN and danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOW then Value is LOW - rule: if heroRole is MAIN and danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOWEST then Value is LOWEST - rule: if heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH with 0.2 - rule: if heroRole is SCOUT then Value is BITLOW - rule: if goldCost is not NONE and goldReward is NONE and goldPreasure is HIGH then Value is LOWEST - rule: if turn is NOW then Value is LOW with 0.3 - rule: if turn is not NOW then Value is LOW with 0.4 - rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST - rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH - rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH - rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and scoutTurnDistance is LOW and armyLoss is LOW then Value is HIGH with 0.5 - rule: if fear is MEDIUM then Value is LOW - rule: if fear is HIGH then Value is LOWEST \ No newline at end of file + rule: if heroRole is MAIN then Value is BASE + rule: if heroRole is SCOUT then Value is BASE + rule: if heroRole is MAIN and armyGrowth is HUGE then Value is HIGH + rule: if heroRole is MAIN and armyGrowth is BIG then Value is BITHIGH + rule: if heroRole is MAIN and strategicalValue is HIGH then Value is HIGHEST \ No newline at end of file From b19ac01bf9c734a83e3fba92980723a1d1ac247c Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 4 Jun 2023 16:02:02 +0300 Subject: [PATCH 068/168] Fuzzy rework, added more defence and gather army routines --- AI/Nullkiller/AIGateway.cpp | 11 +- AI/Nullkiller/Analyzers/ArmyManager.cpp | 24 +- AI/Nullkiller/Analyzers/ArmyManager.h | 33 ++- .../Analyzers/DangerHitMapAnalyzer.cpp | 100 +++++++- .../Analyzers/DangerHitMapAnalyzer.h | 6 +- AI/Nullkiller/Analyzers/HeroManager.cpp | 17 +- AI/Nullkiller/Analyzers/HeroManager.h | 2 + .../Behaviors/CaptureObjectsBehavior.cpp | 14 +- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 235 ++++++++++++------ AI/Nullkiller/Behaviors/DefenceBehavior.h | 5 + .../Behaviors/GatherArmyBehavior.cpp | 49 +++- AI/Nullkiller/CMakeLists.txt | 2 + AI/Nullkiller/Engine/Nullkiller.cpp | 4 +- AI/Nullkiller/Engine/Nullkiller.h | 2 + AI/Nullkiller/Engine/PriorityEvaluator.cpp | 71 ++++-- AI/Nullkiller/Engine/PriorityEvaluator.h | 2 + AI/Nullkiller/Goals/BuyArmy.cpp | 2 +- AI/Nullkiller/Goals/Composition.cpp | 58 ++++- AI/Nullkiller/Goals/Composition.h | 8 +- AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 14 ++ AI/Nullkiller/Goals/RecruitHero.cpp | 21 +- AI/Nullkiller/Goals/RecruitHero.h | 12 +- AI/Nullkiller/Helpers/ArmyFormation.cpp | 68 +++++ AI/Nullkiller/Helpers/ArmyFormation.h | 39 +++ AI/Nullkiller/Markers/HeroExchange.cpp | 2 +- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- AI/Nullkiller/Pathfinding/AINodeStorage.h | 6 +- config/ai/object-priorities.txt | 96 +++++-- 28 files changed, 710 insertions(+), 195 deletions(-) create mode 100644 AI/Nullkiller/Helpers/ArmyFormation.cpp create mode 100644 AI/Nullkiller/Helpers/ArmyFormation.h diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 16cd34849..c3bebecbb 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -29,7 +29,7 @@ namespace NKAI { // our to enemy strength ratio constants -const float SAFE_ATTACK_CONSTANT = 1.2f; +const float SAFE_ATTACK_CONSTANT = 1.1f; const float RETREAT_THRESHOLD = 0.3f; const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; @@ -90,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose) LOG_TRACE(logAi); NET_EVENT_HANDLER; - validateObject(details.id); //enemy hero may have left visible area auto hero = cb->getHero(details.id); + if(!hero) + validateObject(details.id); //enemy hero may have left visible area + const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); @@ -794,10 +796,7 @@ void AIGateway::makeTurn() cb->sendMessage("vcmieagles"); - if(cb->getDate(Date::DAY) == 1) - { - retrieveVisitableObjs(); - } + retrieveVisitableObjs(); #if NKAI_TRACE_LEVEL == 0 try diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 71ce9630f..b8ce631b6 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -238,7 +238,8 @@ std::shared_ptr ArmyManager::getArmyAvailableToBuyAsCCreatureSet( ui64 ArmyManager::howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const + const TResources & availableResources, + uint8_t turn) const { ui64 aivalue = 0; auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources); @@ -259,17 +260,29 @@ std::vector ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her std::vector ArmyManager::getArmyAvailableToBuy( const CCreatureSet * hero, const CGDwelling * dwelling, - TResources availableRes) const + TResources availableRes, + uint8_t turn) const { std::vector creaturesInDwellings; int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount(); + bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7; + + const CGTownInstance * town = dwelling->ID == CGTownInstance::TOWN + ? dynamic_cast(dwelling) + : nullptr; for(int i = dwelling->creatures.size() - 1; i >= 0; i--) { auto ci = infoFromDC(dwelling->creatures[i]); - if(!ci.count || ci.creID == -1) - continue; + if(ci.creID == -1) continue; + + if(i < GameConstants::CREATURES_PER_TOWN && countGrowth) + { + ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth(); + } + + if(!ci.count) continue; SlotID dst = hero->getSlotFor(ci.creID); if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack @@ -282,8 +295,7 @@ std::vector ArmyManager::getArmyAvailableToBuy( vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford - if(!ci.count) - continue; + if(!ci.count) continue; ci.level = i; //this is important for Dungeon Summoning Portal creaturesInDwellings.push_back(ci); diff --git a/AI/Nullkiller/Analyzers/ArmyManager.h b/AI/Nullkiller/Analyzers/ArmyManager.h index b6f27adf9..10848e0d6 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.h +++ b/AI/Nullkiller/Analyzers/ArmyManager.h @@ -45,20 +45,32 @@ public: virtual ui64 howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const = 0; + const TResources & availableResources, + uint8_t turn = 0) const = 0; + virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0; - virtual ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; + virtual ui64 howManyReinforcementsCanGet( + const IBonusBearer * armyCarrier, + const CCreatureSet * target, + const CCreatureSet * source) const = 0; + virtual std::vector getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; virtual std::vector::iterator getWeakestCreature(std::vector & army) const = 0; virtual std::vector getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0; - virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const = 0; + + virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; + virtual std::vector getArmyAvailableToBuy( + const CCreatureSet * hero, + const CGDwelling * dwelling, + TResources availableRes, + uint8_t turn = 0) const = 0; + virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0; virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0; virtual ArmyUpgradeInfo calculateCreaturesUpgrade( const CCreatureSet * army, const CGObjectInstance * upgrader, const TResources & availableResources) const = 0; - virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; virtual std::shared_ptr getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0; }; @@ -74,18 +86,27 @@ private: public: ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {} void update() override; + ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override; ui64 howManyReinforcementsCanBuy( const CCreatureSet * targetArmy, const CGDwelling * dwelling, - const TResources & availableResources) const override; + const TResources & availableResources, + uint8_t turn = 0) const override; + ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override; ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; std::vector getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; std::vector::iterator getWeakestCreature(std::vector & army) const override; std::vector getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override; - std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const override; + std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override; + std::vector getArmyAvailableToBuy( + const CCreatureSet * hero, + const CGDwelling * dwelling, + TResources availableRes, + uint8_t turn = 0) const override; + std::shared_ptr getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override; uint64_t evaluateStackPower(const CCreature * creature, int count) const override; SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 83bb0a02b..0124b0c27 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -19,12 +19,12 @@ HitMapInfo HitMapInfo::NoTreat; void DangerHitMapAnalyzer::updateHitMap() { - if(upToDate) + if(hitMapUpToDate) return; logAi->trace("Update danger hitmap"); - upToDate = true; + hitMapUpToDate = true; auto start = std::chrono::high_resolution_clock::now(); auto cb = ai->cb.get(); @@ -71,8 +71,10 @@ void DangerHitMapAnalyzer::updateHitMap() auto turn = path.turn(); auto & node = hitMap[pos.x][pos.y][pos.z]; - if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1) - || (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn)) + auto newMaxDanger = tileDanger / std::sqrt(turn / 3.0f + 1); + auto currentMaxDanger = node.maximumDanger.danger / std::sqrt(node.maximumDanger.turn / 3.0f + 1); + + if(newMaxDanger > currentMaxDanger) { node.maximumDanger.danger = tileDanger; node.maximumDanger.turn = turn; @@ -104,6 +106,94 @@ void DangerHitMapAnalyzer::updateHitMap() logAi->trace("Danger hit map updated in %ld", timeElapsed(start)); } +void DangerHitMapAnalyzer::calculateTileOwners() +{ + if(tileOwnersUpToDate) return; + + tileOwnersUpToDate = true; + + auto cb = ai->cb.get(); + auto mapSize = ai->cb->getMapSize(); + + tileOwners.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + + std::map townHeroes; + std::map heroTownMap; + PathfinderSettings pathfinderSettings; + + pathfinderSettings.mainTurnDistanceLimit = 3; + + auto addTownHero = [&](const CGTownInstance * town) + { + auto townHero = new CGHeroInstance(); + CRandomGenerator rng; + + townHero->pos = town->pos; + townHero->setOwner(ai->playerID); // lets avoid having multiple colors + townHero->initHero(rng, static_cast(0)); + townHero->initObj(rng); + + heroTownMap[townHero] = town; + townHeroes[townHero] = HeroRole::MAIN; + }; + + for(auto obj : ai->memory->visitableObjs) + { + if(obj && obj->ID == Obj::TOWN) + { + addTownHero(dynamic_cast(obj)); + } + } + + for(auto town : cb->getTownsInfo()) + { + addTownHero(town); + } + + ai->pathfinder->updatePaths(townHeroes, PathfinderSettings()); + + pforeachTilePos(mapSize, [&](const int3 & pos) + { + float ourDistance = std::numeric_limits::max(); + float enemyDistance = std::numeric_limits::max(); + const CGTownInstance * enemyTown = nullptr; + + for(AIPath & path : ai->pathfinder->getPathInfo(pos)) + { + if(!path.targetHero || path.getFirstBlockedAction()) + continue; + + auto town = heroTownMap[path.targetHero]; + + if(town->getOwner() == ai->playerID) + { + vstd::amin(ourDistance, path.movementCost()); + } + else + { + if(enemyDistance > path.movementCost()) + { + enemyDistance = path.movementCost(); + enemyTown = town; + } + } + } + + if(ourDistance == enemyDistance) + { + tileOwners[pos.x][pos.y][pos.z] = PlayerColor::NEUTRAL; + } + else if(!enemyTown || ourDistance < enemyDistance) + { + tileOwners[pos.x][pos.y][pos.z] = ai->playerID; + } + else + { + tileOwners[pos.x][pos.y][pos.z] = enemyTown->getOwner(); + } + }); +} + uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const { int3 tile = path.targetTile(); @@ -144,7 +234,7 @@ const std::set & DangerHitMapAnalyzer::getOneTurnAcces void DangerHitMapAnalyzer::reset() { - upToDate = false; + hitMapUpToDate = false; } } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 660dfd593..e96987b33 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -55,19 +55,23 @@ class DangerHitMapAnalyzer { private: boost::multi_array hitMap; + boost::multi_array tileOwners; std::map> enemyHeroAccessibleObjects; - bool upToDate; + bool hitMapUpToDate = false; + bool tileOwnersUpToDate = false; const Nullkiller * ai; public: DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} void updateHitMap(); + void calculateTileOwners(); uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; const HitMapNode & getTileTreat(const int3 & tile) const; const std::set & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); + void resetTileOwners() { tileOwnersUpToDate = false; } }; } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 089c4436a..6afa78ec7 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -180,6 +180,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const return evaluateFightingStrength(hero); } +bool HeroManager::heroCapReached() const +{ + const bool includeGarnisoned = true; + int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); + + return heroCount >= ALLOWED_ROAMING_HEROES + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); +} + bool HeroManager::canRecruitHero(const CGTownInstance * town) const { if(!town) @@ -191,13 +200,7 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) return false; - const bool includeGarnisoned = true; - int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); - - if(heroCount >= ALLOWED_ROAMING_HEROES) - return false; - - if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + if(heroCapReached()) return false; if(!cb->getAvailableHeroes(town).size()) diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 9c98443f3..84da85b98 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -31,6 +31,7 @@ public: virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0; virtual float evaluateHero(const CGHeroInstance * hero) const = 0; virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0; + virtual bool heroCapReached() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0; }; @@ -71,6 +72,7 @@ public: float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override; float evaluateHero(const CGHeroInstance * hero) const override; bool canRecruitHero(const CGTownInstance * t = nullptr) const override; + bool heroCapReached() const override; const CGHeroInstance * findHeroWithGrail() const override; private: diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index e63e26c3a..132861467 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector tasks.reserve(paths.size()); - const AIPath * closestWay = nullptr; + std::unordered_map closestWaysByRole; std::vector waysToVisitObj; for(auto & path : paths) @@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); - if(heroRole == HeroRole::SCOUT - && (!closestWay || closestWay->movementCost() > path.movementCost())) + auto & closestWay = closestWaysByRole[heroRole]; + + if(!closestWay || closestWay->movementCost() > path.movementCost()) { closestWay = &path; } @@ -142,9 +143,12 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector } } - if(closestWay) + for(auto way : waysToVisitObj) { - for(auto way : waysToVisitObj) + auto heroRole = ai->nullkiller->heroManager->getHeroRole(way->getPath().targetHero); + auto closestWay = closestWaysByRole[heroRole]; + + if(closestWay) { way->closestWayRatio = closestWay->movementCost() / way->getPath().movementCost(); diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 2a1bbf4c5..0f971d290 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -60,27 +60,27 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(town->garrisonHero) { - if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get())) + if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) { - if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) - { - logAi->trace( - "Extracting hero %s from garrison of town %s", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); + logAi->trace( + "Hero %s in garrison of town %s is suposed to defend the town", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); - tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - - return; - } + return; } - logAi->trace( - "Hero %s in garrison of town %s is suposed to defend the town", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); + if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + { + logAi->trace( + "Extracting hero %s from garrison of town %s", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); - return; + tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); + + return; + } } if(!treatNode.fastestDanger.hero) @@ -113,11 +113,21 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta for(AIPath & path : paths) { - if(town->visitingHero && path.targetHero != town->visitingHero.get()) - continue; - - if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength()) - continue; + if(town->visitingHero && path.targetHero == town->visitingHero.get()) + { + if(path.getHeroStrength() < town->visitingHero->getHeroStrength()) + continue; + } + else if(town->garrisonHero && path.targetHero == town->garrisonHero.get()) + { + if(path.getHeroStrength() < town->visitingHero->getHeroStrength()) + continue; + } + else + { + if(town->visitingHero) + continue; + } if(treat.hero.validAndSet() && treat.turn <= 1 @@ -158,53 +168,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(treatIsUnderControl) continue; - if(!town->visitingHero - && town->hasBuilt(BuildingID::TAVERN) - && cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) - { - auto heroesInTavern = cb->getAvailableHeroes(town); - - for(auto hero : heroesInTavern) - { - if(hero->getTotalStrength() > treat.danger) - { - auto myHeroes = cb->getHeroesInfo(); - - if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES) - { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName()); -#endif - tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1))); - continue; - } - else - { - const CGHeroInstance * weakestHero = nullptr; - - for(auto existingHero : myHeroes) - { - if(ai->nullkiller->isHeroLocked(existingHero) - || existingHero->getArmyStrength() > hero->getArmyStrength() - || ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN - || existingHero->movementPointsRemaining() - || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) - continue; - - if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) - { - weakestHero = existingHero; - } - - if(weakestHero) - { - tasks.push_back(Goals::sptr(Goals::DismissHero(weakestHero))); - } - } - } - } - } - } + evaluateRecruitingHero(tasks, treat, town); if(paths.empty()) { @@ -275,9 +239,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta tasks.push_back( Goals::sptr(Composition() .addNext(DefendTown(town, treat, path)) - .addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get())) - .addNext(ExecuteHeroChain(path, town)) - .addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE)))); + .addNextSequence({ + sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())), + sptr(ExecuteHeroChain(path, town)), + sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE)) + }))); continue; } @@ -313,15 +279,45 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } } - -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Move %s to defend town %s", - path.targetHero->getObjectName(), - town->getObjectName()); -#endif Composition composition; - composition.addNext(DefendTown(town, treat, path)).addNext(ExecuteHeroChain(path, town)); + composition.addNext(DefendTown(town, treat, path)); + TGoalVec sequence; + + if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) + { + if(town->garrisonHero) + { + if(ai->nullkiller->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT + && town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20) + { + if(path.turn() == 0) + sequence.push_back(sptr(DismissHero(town->visitingHero.get()))); + } + else + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + continue; + } + } + else if(path.turn() == 0) + { + sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get()))); + } + } + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Move %s to defend town %s", + path.targetHero->getObjectName(), + town->getObjectName()); +#endif + + sequence.push_back(sptr(ExecuteHeroChain(path, town))); + composition.addNextSequence(sequence); auto firstBlockedAction = path.getFirstBlockedAction(); if(firstBlockedAction) @@ -350,4 +346,87 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->debug("Found %d tasks", tasks.size()); } +void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const +{ + if(town->hasBuilt(BuildingID::TAVERN) + && cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) + { + auto heroesInTavern = cb->getAvailableHeroes(town); + + for(auto hero : heroesInTavern) + { + if(hero->getTotalStrength() < treat.danger) + continue; + + auto myHeroes = cb->getHeroesInfo(); + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName()); +#endif + bool needSwap = false; + const CGHeroInstance * heroToDismiss = nullptr; + + if(town->visitingHero) + { + if(!town->garrisonHero) + needSwap = true; + else + { + if(town->visitingHero->getArmyStrength() < town->garrisonHero->getArmyStrength()) + { + if(town->visitingHero->getArmyStrength() >= hero->getArmyStrength()) + continue; + + heroToDismiss = town->visitingHero.get(); + } + else if(town->garrisonHero->getArmyStrength() >= hero->getArmyStrength()) + continue; + else + { + needSwap = true; + heroToDismiss = town->garrisonHero.get(); + } + } + } + else if(ai->nullkiller->heroManager->heroCapReached()) + { + const CGHeroInstance * weakestHero = nullptr; + + for(auto existingHero : myHeroes) + { + if(ai->nullkiller->isHeroLocked(existingHero) + || existingHero->getArmyStrength() > hero->getArmyStrength() + || ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN + || existingHero->movementPointsRemaining() + || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) + continue; + + if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) + { + weakestHero = existingHero; + } + } + + if(!weakestHero) + continue; + + heroToDismiss = weakestHero; + } + + TGoalVec sequence; + Goals::Composition recruitHeroComposition; + + if(needSwap) + sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get()))); + + if(heroToDismiss) + sequence.push_back(sptr(DismissHero(heroToDismiss))); + + sequence.push_back(sptr(Goals::RecruitHero(town, hero))); + + tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence))); + } + } +} + } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.h b/AI/Nullkiller/Behaviors/DefenceBehavior.h index bd606cd36..b9b7c486e 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.h +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.h @@ -15,8 +15,12 @@ namespace NKAI { + +struct HitMapInfo; + namespace Goals { + class DefenceBehavior : public CGoal { public: @@ -35,6 +39,7 @@ namespace Goals private: void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const; + void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index c2622cdc0..66416df54 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -16,6 +16,7 @@ #include "../Markers/ArmyUpgrade.h" #include "GatherArmyBehavior.h" #include "../AIUtility.h" +#include "../Goals/ExchangeSwapTownHeroes.h" namespace NKAI { @@ -78,20 +79,27 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her for(const AIPath & path : paths) { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Path found %s", path.toString()); + logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif - if(path.containsHero(hero)) continue; - - if(path.turn() == 0 && hero->inTownGarrison) + if(path.containsHero(hero)) { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString()); +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Selfcontaining path. Ignore"); #endif continue; } - if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) + bool garrisoned = false; + + if(path.turn() == 0 && hero->inTownGarrison) + { +#if NKAI_TRACE_LEVEL >= 1 + garrisoned = true; +#endif + } + + if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength()); @@ -172,7 +180,21 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her exchangePath.closestWayRatio = 1; composition.addNext(heroExchange); - composition.addNext(exchangePath); + + if(garrisoned && path.turn() == 0) + { + auto lockReason = ai->nullkiller->getHeroLockedReason(hero); + + composition.addNextSequence({ + sptr(ExchangeSwapTownHeroes(hero->visitedTown)), + sptr(exchangePath), + sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason)) + }); + } + else + { + composition.addNext(exchangePath); + } auto blockedAction = path.getFirstBlockedAction(); @@ -221,7 +243,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) for(const AIPath & path : paths) { #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Path found %s", path.toString()); + logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero) { @@ -267,7 +289,14 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) ai->nullkiller->armyManager->howManyReinforcementsCanGet( path.targetHero, path.heroArmy, - upgrader->getUpperArmy()); + upgrader->getUpperArmy()); + + upgrade.upgradeValue += + ai->nullkiller->armyManager->howManyReinforcementsCanBuy( + path.heroArmy, + upgrader, + ai->nullkiller->getFreeResources(), + path.turn()); } auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index b1abec00c..a6560989f 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -52,6 +52,7 @@ set(Nullkiller_SRCS Behaviors/BuildingBehavior.cpp Behaviors/GatherArmyBehavior.cpp Behaviors/ClusterBehavior.cpp + Helpers/ArmyFormation.cpp AIGateway.cpp ) @@ -114,6 +115,7 @@ set(Nullkiller_HEADERS Behaviors/BuildingBehavior.h Behaviors/GatherArmyBehavior.h Behaviors/ClusterBehavior.h + Helpers/ArmyFormation.h AIGateway.h ) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index c5aa3324f..55976d638 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -61,6 +61,7 @@ void Nullkiller::init(std::shared_ptr cb, PlayerColor playerID) armyManager.reset(new ArmyManager(cb.get(), this)); heroManager.reset(new HeroManager(cb.get(), this)); decomposer.reset(new DeepDecomposer()); + armyFormation.reset(new ArmyFormation(cb, this)); } Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const @@ -137,6 +138,7 @@ void Nullkiller::updateAiState(int pass, bool fast) { memory->removeInvisibleObjects(cb.get()); + dangerHitMap->calculateTileOwners(); dangerHitMap->updateHitMap(); boost::this_thread::interruption_point(); @@ -222,7 +224,7 @@ void Nullkiller::makeTurn() boost::lock_guard sharedStorageLock(AISharedStorage::locker); const int MAX_DEPTH = 10; - const float FAST_TASK_MINIMAL_PRIORITY = 0.7; + const float FAST_TASK_MINIMAL_PRIORITY = 0.7f; resetAiState(); diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 1b5e513e6..d47e634ea 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -18,6 +18,7 @@ #include "../Analyzers/ArmyManager.h" #include "../Analyzers/HeroManager.h" #include "../Analyzers/ObjectClusterizer.h" +#include "../Helpers/ArmyFormation.h" namespace NKAI { @@ -67,6 +68,7 @@ public: std::unique_ptr memory; std::unique_ptr dangerEvaluator; std::unique_ptr decomposer; + std::unique_ptr armyFormation; PlayerColor playerID; std::shared_ptr cb; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index eabd52403..66ef95c7c 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -23,6 +23,7 @@ #include "../Goals/ExecuteHeroChain.h" #include "../Goals/BuildThis.h" #include "../Goals/ExchangeSwapTownHeroes.h" +#include "../Goals/DismissHero.h" #include "../Markers/UnlockCluster.h" #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" @@ -33,6 +34,7 @@ namespace NKAI #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us +const float MIN_CRITICAL_VALUE = 2.0f; EvaluationContext::EvaluationContext(const Nullkiller * ai) : movementCost(0.0), @@ -54,6 +56,11 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai) { } +void EvaluationContext::addNonCriticalStrategicalValue(float value) +{ + vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE)); +} + PriorityEvaluator::~PriorityEvaluator() { delete engine; @@ -399,7 +406,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy 2. The formula quickly approaches 1.0 as hero level increases, but higher level always means higher value and the minimal value for level 1 hero is 0.5 */ - return std::min(1.0f, objectValue * 0.9f + (1.0f - (1.0f / (1 + enemy->level)))); + return std::min(1.5f, objectValue * 0.9f + (1.5f - (1.5f / (1 + enemy->level)))); } float RewardEvaluator::getResourceRequirementStrength(int resType) const @@ -640,7 +647,8 @@ public: uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(); - evaluationContext.strategicalValue += 0.5f * armyStrength / heroExchange.hero.get()->getArmyStrength(); + evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength()); + evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get()); } }; @@ -657,7 +665,7 @@ public: uint64_t upgradeValue = armyUpgrade.getUpgradeValue(); evaluationContext.armyReward += upgradeValue; - evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength(); + evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); } }; @@ -712,19 +720,21 @@ public: auto armyIncome = townArmyIncome(town); auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; - auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f; + auto strategicalValue = std::sqrt(armyIncome / 60000.0f) + dailyIncome / 10000.0f; if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) - strategicalValue = 1; + vstd::amax(evaluationContext.strategicalValue, 10.0); float multiplier = 1; if(treat.turn < defendTown.getTurn()) multiplier /= 1 + (defendTown.getTurn() - treat.turn); - evaluationContext.armyReward += armyIncome * multiplier; + multiplier /= 1.0f + treat.turn / 5.0f; + + evaluationContext.armyGrowth += armyIncome * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier; - evaluationContext.strategicalValue += strategicalValue * multiplier; + evaluationContext.addNonCriticalStrategicalValue(strategicalValue * multiplier); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } @@ -770,19 +780,22 @@ public: auto army = path.heroArmy; const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false); + auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr); + + if(heroRole == HeroRole::MAIN) + evaluationContext.heroRole = heroRole; if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES) { evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); - evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole); - evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target); + evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole); + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target)); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); } vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength()); - evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength()); vstd::amax(evaluationContext.turn, path.turn()); } @@ -822,7 +835,7 @@ public: evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost; evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; - evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target) / boost; + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost; @@ -860,6 +873,31 @@ public: } }; +class DismissHeroContextBuilder : public IEvaluationContextBuilder +{ +private: + const Nullkiller * ai; + +public: + DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {} + + virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + { + if(task->goalType != Goals::DISMISS_HERO) + return; + + Goals::DismissHero & dismissCommand = dynamic_cast(*task); + const CGHeroInstance * dismissedHero = dismissCommand.getHero().get(); + + auto role = ai->heroManager->getHeroRole(dismissedHero); + auto mpLeft = dismissedHero->movement; + + evaluationContext.movementCost += mpLeft; + evaluationContext.movementCostByRole[role] += mpLeft; + evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero); + } +}; + class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder { public: @@ -878,31 +916,31 @@ public: if(bi.creatureID != CreatureID::NONE) { - evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0; + evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0); if(bi.baseCreatureID == bi.creatureID) { - evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount); evaluationContext.armyReward += bi.armyStrength; } else { auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi); - evaluationContext.strategicalValue += potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount); evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount; } } else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE) { - evaluationContext.strategicalValue += buildThis.town->creatures.size() * 0.2f; + evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f); evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2; } else { auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure(); - evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount; + evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount); } if(bi.notEnoughRes && bi.prerequisitesCount == 1) @@ -934,6 +972,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai) evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); + evaluationContextBuilders.push_back(std::make_shared(ai)); } EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index a07ebf3f6..8f7f478f7 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -66,6 +66,8 @@ struct DLL_EXPORT EvaluationContext float enemyHeroDangerRatio; EvaluationContext(const Nullkiller * ai); + + void addNonCriticalStrategicalValue(float value); }; class IEvaluationContextBuilder diff --git a/AI/Nullkiller/Goals/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index ddbabe936..f2e4aca05 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -71,7 +71,7 @@ void BuyArmy::accept(AIGateway * ai) throw cannotFulfillGoalException("No creatures to buy."); } - if(town->visitingHero) + if(town->visitingHero && !town->garrisonHero) { ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get()); } diff --git a/AI/Nullkiller/Goals/Composition.cpp b/AI/Nullkiller/Goals/Composition.cpp index a0a487820..30d3791f9 100644 --- a/AI/Nullkiller/Goals/Composition.cpp +++ b/AI/Nullkiller/Goals/Composition.cpp @@ -31,9 +31,17 @@ std::string Composition::toString() const { std::string result = "Composition"; - for(auto goal : subtasks) + for(auto step : subtasks) { - result += " " + goal->toString(); + result += "["; + for(auto goal : step) + { + if(goal->isElementar()) + result += goal->toString() + " => "; + else + result += goal->toString() + ", "; + } + result += "] "; } return result; @@ -41,17 +49,34 @@ std::string Composition::toString() const void Composition::accept(AIGateway * ai) { - taskptr(*subtasks.back())->accept(ai); + for(auto task : subtasks.back()) + { + if(task->isElementar()) + { + taskptr(*task)->accept(ai); + } + else + { + break; + } + } } TGoalVec Composition::decompose() const { - return subtasks; + TGoalVec result; + + for(const TGoalVec & step : subtasks) + vstd::concatenate(result, step); + + return result; } -Composition & Composition::addNext(const AbstractGoal & goal) +Composition & Composition::addNextSequence(const TGoalVec & taskSequence) { - return addNext(sptr(goal)); + subtasks.push_back(taskSequence); + + return *this; } Composition & Composition::addNext(TSubgoal goal) @@ -64,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal) } else { - subtasks.push_back(goal); + subtasks.push_back({goal}); } return *this; } +Composition & Composition::addNext(const AbstractGoal & goal) +{ + return addNext(sptr(goal)); +} + bool Composition::isElementar() const { - return subtasks.back()->isElementar(); + return subtasks.back().front()->isElementar(); } int Composition::getHeroExchangeCount() const { - return isElementar() ? taskptr(*subtasks.back())->getHeroExchangeCount() : 0; + auto result = 0; + + for(auto task : subtasks.back()) + { + if(task->isElementar()) + { + result += taskptr(*task)->getHeroExchangeCount(); + } + } + + return result; } } diff --git a/AI/Nullkiller/Goals/Composition.h b/AI/Nullkiller/Goals/Composition.h index f7de02bb0..ecb4a1ab9 100644 --- a/AI/Nullkiller/Goals/Composition.h +++ b/AI/Nullkiller/Goals/Composition.h @@ -18,7 +18,7 @@ namespace Goals class DLL_EXPORT Composition : public ElementarGoal { private: - TGoalVec subtasks; + std::vector subtasks; // things we want to do now public: Composition() @@ -26,16 +26,12 @@ namespace Goals { } - Composition(TGoalVec subtasks) - : ElementarGoal(Goals::COMPOSITION), subtasks(subtasks) - { - } - virtual bool operator==(const Composition & other) const override; virtual std::string toString() const override; void accept(AIGateway * ai) override; Composition & addNext(const AbstractGoal & goal); Composition & addNext(TSubgoal goal); + Composition & addNextSequence(const TGoalVec & taskSequence); virtual TGoalVec decompose() const override; virtual bool isElementar() const override; virtual int getHeroExchangeCount() const override; diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 41b0d4e49..d367f96b4 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -52,6 +52,20 @@ void ExecuteHeroChain::accept(AIGateway * ai) ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setTargetObject(objid); + auto targetObject = ai->myCb->getObj(static_cast(objid), false); + + if(chainPath.turn() == 0 && targetObject && targetObject->ID == Obj::TOWN) + { + auto relations = ai->myCb->getPlayerRelations(ai->playerID, targetObject->getOwner()); + + if(relations == PlayerRelations::ENEMIES) + { + ai->nullkiller->armyFormation->rearrangeArmyForSiege( + dynamic_cast(targetObject), + chainPath.targetHero); + } + } + std::set blockedIndexes; for(int i = chainPath.nodes.size() - 1; i >= 0; i--) diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index 9dc63020b..e787c7529 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -24,7 +24,10 @@ using namespace Goals; std::string RecruitHero::toString() const { - return "Recruit hero at " + town->getNameTranslated(); + if(heroToBuy) + return "Recruit " + heroToBuy->getNameTranslated() + " at " + town->getNameTranslated(); + else + return "Recruit hero at " + town->getNameTranslated(); } void RecruitHero::accept(AIGateway * ai) @@ -45,20 +48,20 @@ void RecruitHero::accept(AIGateway * ai) throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); } - auto heroToHire = heroes[0]; + auto heroToHire = heroToBuy; - for(auto hero : heroes) + if(!heroToHire) { - if(objid == hero->id.getNum()) + for(auto hero : heroes) { - heroToHire = hero; - break; + if(!heroToHire || hero->getTotalStrength() > heroToHire->getTotalStrength()) + heroToHire = hero; } - - if(hero->getTotalStrength() > heroToHire->getTotalStrength()) - heroToHire = hero; } + if(!heroToHire) + throw cannotFulfillGoalException("No hero to hire!"); + if(t->visitingHero) { cb->swapGarrisonHero(t); diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 34a968f33..78c6b0867 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -22,18 +22,20 @@ namespace Goals { class DLL_EXPORT RecruitHero : public ElementarGoal { + private: + const CGHeroInstance * heroToBuy; + public: RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy) - : RecruitHero(townWithTavern) + : ElementarGoal(Goals::RECRUIT_HERO), heroToBuy(heroToBuy) { - objid = heroToBuy->id.getNum(); + town = townWithTavern; + priority = 1; } RecruitHero(const CGTownInstance * townWithTavern) - : ElementarGoal(Goals::RECRUIT_HERO) + : RecruitHero(townWithTavern, nullptr) { - priority = 1; - town = townWithTavern; } virtual bool operator==(const RecruitHero & other) const override diff --git a/AI/Nullkiller/Helpers/ArmyFormation.cpp b/AI/Nullkiller/Helpers/ArmyFormation.cpp new file mode 100644 index 000000000..464cb10d0 --- /dev/null +++ b/AI/Nullkiller/Helpers/ArmyFormation.cpp @@ -0,0 +1,68 @@ +/* +* ArmyFormation.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#include "StdInc.h" +#include "ArmyFormation.h" +#include "../../../lib/mapObjects/CGTownInstance.h" + +namespace NKAI +{ + +void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker) +{ + auto freeSlots = attacker->getFreeSlotsQueue(); + + while(!freeSlots.empty()) + { + auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair & slot) -> int + { + return slot.second->getCount() == 1 + ? std::numeric_limits::max() + : slot.second->getCreatureID().toCreature()->getAIValue(); + }); + + if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1) + { + break; + } + + cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1); + freeSlots.pop(); + } + + if(town->fortLevel() > CGTownInstance::FORT) + { + std::vector stacks; + + for(auto slot : attacker->Slots()) + stacks.push_back(slot.second); + + boost::sort( + stacks, + [](CStackInstance * slot1, CStackInstance * slot2) -> bool + { + auto cre1 = slot1->getCreatureID().toCreature(); + auto cre2 = slot2->getCreatureID().toCreature(); + auto flying = cre1->hasBonusOfType(BonusType::FLYING) - cre2->hasBonusOfType(BonusType::FLYING); + + if(flying != 0) return flying < 0; + else return cre1->getAIValue() < cre2->getAIValue(); + }); + + for(int i = 0; i < stacks.size(); i++) + { + auto pos = vstd::findKey(attacker->Slots(), stacks[i]); + + if(pos.getNum() != i) + cb->swapCreatures(attacker, attacker, static_cast(i), pos); + } + } +} + +} diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h new file mode 100644 index 000000000..e5a900c01 --- /dev/null +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -0,0 +1,39 @@ +/* +* ArmyFormation.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#pragma once + +#include "../AIUtility.h" + +#include "../../../lib/GameConstants.h" +#include "../../../lib/VCMI_Lib.h" +#include "../../../lib/CTownHandler.h" +#include "../../../lib/CBuildingHandler.h" + +namespace NKAI +{ + +struct HeroPtr; +class AIGateway; +class FuzzyHelper; +class Nullkiller; + +class DLL_EXPORT ArmyFormation +{ +private: + std::shared_ptr cb; //this is enough, but we downcast from CCallback + const Nullkiller * ai; + +public: + ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB), ai(ai) {} + + void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker); +}; + +} diff --git a/AI/Nullkiller/Markers/HeroExchange.cpp b/AI/Nullkiller/Markers/HeroExchange.cpp index 562215bb3..499122327 100644 --- a/AI/Nullkiller/Markers/HeroExchange.cpp +++ b/AI/Nullkiller/Markers/HeroExchange.cpp @@ -29,7 +29,7 @@ bool HeroExchange::operator==(const HeroExchange & other) const std::string HeroExchange::toString() const { - return "Hero exchange " + exchangePath.toString(); + return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString(); } uint64_t HeroExchange::getReinforcementArmyStrength() const diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index c0d650464..2fbd50a72 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,7 +879,7 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison) + if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first)) continue; uint64_t mask = FirstActorMask << actors.size(); diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 22c2d6b21..02364ad11 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -11,7 +11,7 @@ #pragma once #define NKAI_PATHFINDER_TRACE_LEVEL 0 -#define NKAI_TRACE_LEVEL 0 +#define NKAI_TRACE_LEVEL 2 #include "../../../lib/pathfinder/CGPathNode.h" #include "../../../lib/pathfinder/INodeStorage.h" @@ -24,8 +24,8 @@ namespace NKAI { - const int SCOUT_TURN_DISTANCE_LIMIT = 3; - const int MAIN_TURN_DISTANCE_LIMIT = 5; + const int SCOUT_TURN_DISTANCE_LIMIT = 5; + const int MAIN_TURN_DISTANCE_LIMIT = 10; namespace AIPathfinding { diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index 1714c8477..13ec86617 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -6,9 +6,9 @@ InputVariable: mainTurnDistance range: 0.000 10.000 lock-range: true term: LOWEST Ramp 0.250 0.000 - term: LOW Discrete 0.000 1.000 0.500 0.800 1.000 0.000 - term: MEDIUM Discrete 0.000 0.000 0.500 0.200 1.000 1.000 3.000 0.000 - term: LONG Discrete 1.000 0.000 1.500 0.200 3.000 0.800 10.000 1.000 + term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000 + term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000 + term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800 InputVariable: scoutTurnDistance description: distance to tile in turns enabled: true @@ -43,6 +43,7 @@ InputVariable: armyLoss term: LOW Ramp 0.200 0.000 term: MEDIUM Triangle 0.000 0.200 0.500 term: HIGH Ramp 0.200 0.500 + term: ALL Ramp 0.700 1.000 InputVariable: heroRole enabled: true range: -0.100 1.100 @@ -82,13 +83,14 @@ InputVariable: closestHeroRatio InputVariable: strategicalValue description: Some abstract long term benefit non gold or army or skill enabled: true - range: 0.000 1.000 + range: 0.000 3.000 lock-range: false term: NONE Ramp 0.200 0.000 term: LOWEST Triangle 0.000 0.010 0.250 - term: LOW Triangle 0.000 0.250 0.700 - term: MEDIUM Triangle 0.250 0.700 1.000 - term: HIGH Ramp 0.700 1.000 + term: LOW Triangle 0.000 0.250 1.000 + term: MEDIUM Triangle 0.250 1.000 2.000 + term: HIGH Triangle 1.000 2.000 3.000 + term: CRITICAL Ramp 2.000 3.000 InputVariable: goldPreasure description: Ratio between weekly army cost and gold income enabled: true @@ -132,21 +134,22 @@ InputVariable: armyGrowth term: HUGE Ramp 8000.000 20000.000 OutputVariable: Value enabled: true - range: -1.500 2.000 + range: -1.500 2.500 lock-range: false aggregation: AlgebraicSum defuzzifier: Centroid 100 default: 0.500 lock-previous: false - term: WORST Binary -1.000 -inf 0.500 + term: WORST Binary -1.000 -inf 0.700 term: BAD Rectangle -1.000 -0.700 0.500 - term: BASE Rectangle -0.200 0.200 0.400 - term: LOW Rectangle 1.110 1.190 0.320 - term: HIGHEST Discrete 0.300 0.000 0.300 1.000 0.600 1.000 0.600 0.000 1.700 0.000 1.700 1.000 2.000 1.000 2.000 0.000 0.500 - term: HIGH Discrete 0.600 0.000 0.600 1.000 0.850 1.000 0.850 0.000 1.450 0.000 1.450 1.000 1.700 1.000 1.700 0.000 0.400 - term: BITHIGH Discrete 0.850 0.000 0.850 1.000 1.000 1.000 1.000 0.000 1.300 0.000 1.300 1.000 1.450 1.000 1.450 0.000 0.350 - term: MEDIUM Discrete 1.000 0.000 1.000 1.000 1.100 1.000 1.100 0.000 1.200 0.000 1.200 1.000 1.300 1.000 1.300 0.000 0.330 -RuleBlock: gold reward + term: BASE Rectangle -0.200 0.200 0.350 + term: MEDIUM Rectangle 0.910 1.090 0.500 + term: SMALL Rectangle 0.960 1.040 0.600 + term: BITHIGH Rectangle 0.850 1.150 0.400 + term: HIGH Rectangle 0.750 1.250 0.400 + term: HIGHEST Rectangle 0.500 1.500 0.350 + term: CRITICAL Ramp 0.500 2.000 0.500 +RuleBlock: basic enabled: true conjunction: AlgebraicProduct disjunction: AlgebraicSum @@ -154,6 +157,61 @@ RuleBlock: gold reward activation: General rule: if heroRole is MAIN then Value is BASE rule: if heroRole is SCOUT then Value is BASE - rule: if heroRole is MAIN and armyGrowth is HUGE then Value is HIGH - rule: if heroRole is MAIN and armyGrowth is BIG then Value is BITHIGH - rule: if heroRole is MAIN and strategicalValue is HIGH then Value is HIGHEST \ No newline at end of file + rule: if heroRole is MAIN and armyGrowth is HUGE and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if armyLoss is ALL then Value is WORST + rule: if turn is not NOW then Value is BAD with 0.1 + rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST + rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD + rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD +RuleBlock: strategicalValue + enabled: true + conjunction: AlgebraicProduct + disjunction: NormalizedSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and strategicalValue is HIGH and turn is NOW then Value is HIGHEST + rule: if heroRole is MAIN and strategicalValue is HIGH and turn is not NOW and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5 + rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is NOW then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.5 + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LOW then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is not NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is NOW then Value is HIGHEST + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5 + rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD + rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7 + rule: if strategicalValue is CRITICAL then Value is CRITICAL +RuleBlock: armyReward + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LOW then Value is HIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL \ No newline at end of file From 6ba74f02bc0581af07c80ebb16cc5d89e3a4dc56 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 11 Jun 2023 19:21:50 +0300 Subject: [PATCH 069/168] NKAI: playing around with defence --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 43 ++++++++- AI/Nullkiller/Analyzers/ArmyManager.h | 9 +- .../Analyzers/DangerHitMapAnalyzer.cpp | 93 +++++++++++++++---- .../Analyzers/DangerHitMapAnalyzer.h | 13 ++- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 24 +++-- .../Behaviors/GatherArmyBehavior.cpp | 68 +++++++++++--- .../Behaviors/RecruitHeroBehavior.cpp | 21 +++++ AI/Nullkiller/Engine/PriorityEvaluator.cpp | 82 ++++++++-------- AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + AI/Nullkiller/Markers/ArmyUpgrade.cpp | 7 ++ AI/Nullkiller/Markers/ArmyUpgrade.h | 1 + AI/Nullkiller/Markers/DefendTown.cpp | 4 +- AI/Nullkiller/Markers/DefendTown.h | 5 +- 13 files changed, 286 insertions(+), 85 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index b8ce631b6..359286a02 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -33,6 +33,45 @@ public: } }; +void ArmyUpgradeInfo::addArmyToBuy(std::vector army) +{ + for(auto slot : army) + { + resultingArmy.push_back(slot); + + upgradeValue += slot.power; + upgradeCost += slot.creature->getFullRecruitCost() * slot.count; + } +} + +void ArmyUpgradeInfo::addArmyToGet(std::vector army) +{ + for(auto slot : army) + { + resultingArmy.push_back(slot); + + upgradeValue += slot.power; + } +} + +std::vector ArmyManager::toSlotInfo(std::vector army) const +{ + std::vector result; + + for(auto i : army) + { + SlotInfo slot; + + slot.creature = VLC->creh->objects[i.cre->getId()]; + slot.count = i.count; + slot.power = evaluateStackPower(i.cre, i.count); + + result.push_back(slot); + } + + return result; +} + uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const { return howManyReinforcementsCanGet(hero, hero, source); @@ -136,7 +175,7 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, { if(vstd::contains(allowedFactions, slot.creature->getFaction())) { - auto slotID = newArmyInstance.getSlotFor(slot.creature); + auto slotID = newArmyInstance.getSlotFor(slot.creature->getId()); if(slotID.validSlot()) { @@ -319,7 +358,7 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, return newArmy > oldArmy ? newArmy - oldArmy : 0; } -uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const +uint64_t ArmyManager::evaluateStackPower(const Creature * creature, int count) const { return creature->getAIValue() * count; } diff --git a/AI/Nullkiller/Analyzers/ArmyManager.h b/AI/Nullkiller/Analyzers/ArmyManager.h index 10848e0d6..1617bd1bd 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.h +++ b/AI/Nullkiller/Analyzers/ArmyManager.h @@ -34,6 +34,9 @@ struct ArmyUpgradeInfo std::vector resultingArmy; uint64_t upgradeValue = 0; TResources upgradeCost; + + void addArmyToBuy(std::vector army); + void addArmyToGet(std::vector army); }; class DLL_EXPORT IArmyManager //: public: IAbstractManager @@ -57,6 +60,7 @@ public: virtual std::vector getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0; virtual std::vector::iterator getWeakestCreature(std::vector & army) const = 0; virtual std::vector getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0; + virtual std::vector toSlotInfo(std::vector creatures) const = 0; virtual std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0; virtual std::vector getArmyAvailableToBuy( @@ -65,7 +69,7 @@ public: TResources availableRes, uint8_t turn = 0) const = 0; - virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0; + virtual uint64_t evaluateStackPower(const Creature * creature, int count) const = 0; virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0; virtual ArmyUpgradeInfo calculateCreaturesUpgrade( const CCreatureSet * army, @@ -99,6 +103,7 @@ public: std::vector getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override; std::vector::iterator getWeakestCreature(std::vector & army) const override; std::vector getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override; + std::vector toSlotInfo(std::vector creatures) const override; std::vector getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override; std::vector getArmyAvailableToBuy( @@ -108,7 +113,7 @@ public: uint8_t turn = 0) const override; std::shared_ptr getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override; - uint64_t evaluateStackPower(const CCreature * creature, int count) const override; + uint64_t evaluateStackPower(const Creature * creature, int count) const override; SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override; ArmyUpgradeInfo calculateCreaturesUpgrade( const CCreatureSet * army, diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 0124b0c27..7bd330137 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -17,6 +17,11 @@ namespace NKAI HitMapInfo HitMapInfo::NoTreat; +double HitMapInfo::value() const +{ + return danger / std::sqrt(turn / 3.0f + 1); +} + void DangerHitMapAnalyzer::updateHitMap() { if(hitMapUpToDate) @@ -29,8 +34,12 @@ void DangerHitMapAnalyzer::updateHitMap() auto cb = ai->cb.get(); auto mapSize = ai->cb->getMapSize(); - hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + + if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z) + hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + enemyHeroAccessibleObjects.clear(); + townTreats.clear(); std::map> heroes; @@ -67,29 +76,26 @@ void DangerHitMapAnalyzer::updateHitMap() if(path.getFirstBlockedAction()) continue; - auto tileDanger = path.getHeroStrength(); - auto turn = path.turn(); auto & node = hitMap[pos.x][pos.y][pos.z]; - auto newMaxDanger = tileDanger / std::sqrt(turn / 3.0f + 1); - auto currentMaxDanger = node.maximumDanger.danger / std::sqrt(node.maximumDanger.turn / 3.0f + 1); + HitMapInfo newTreat; - if(newMaxDanger > currentMaxDanger) + newTreat.hero = path.targetHero; + newTreat.turn = path.turn(); + newTreat.danger = path.getHeroStrength(); + + if(newTreat.value() > node.maximumDanger.value()) { - node.maximumDanger.danger = tileDanger; - node.maximumDanger.turn = turn; - node.maximumDanger.hero = path.targetHero; + node.maximumDanger = newTreat; } - if(turn < node.fastestDanger.turn - || (turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger)) + if(newTreat.turn < node.fastestDanger.turn + || (newTreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newTreat.danger)) { - node.fastestDanger.danger = tileDanger; - node.fastestDanger.turn = turn; - node.fastestDanger.hero = path.targetHero; + node.fastestDanger = newTreat; } - if(turn == 0) + if(newTreat.turn == 0) { auto objects = cb->getVisitableObjs(pos, false); @@ -97,6 +103,26 @@ void DangerHitMapAnalyzer::updateHitMap() { if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) enemyHeroAccessibleObjects[path.targetHero].insert(obj); + + if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) + { + auto & treats = townTreats[obj->id]; + auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool + { + return i.hero.hid == path.targetHero->id; + }); + + if(treat == treats.end()) + { + treats.emplace_back(); + treat = std::prev(treats.end(), 1); + } + + if(newTreat.value() > treat->value()) + { + *treat = newTreat; + } + } } } } @@ -115,7 +141,8 @@ void DangerHitMapAnalyzer::calculateTileOwners() auto cb = ai->cb.get(); auto mapSize = ai->cb->getMapSize(); - tileOwners.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); + if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z) + hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); std::map townHeroes; std::map heroTownMap; @@ -157,6 +184,7 @@ void DangerHitMapAnalyzer::calculateTileOwners() float ourDistance = std::numeric_limits::max(); float enemyDistance = std::numeric_limits::max(); const CGTownInstance * enemyTown = nullptr; + const CGTownInstance * ourTown = nullptr; for(AIPath & path : ai->pathfinder->getPathInfo(pos)) { @@ -167,7 +195,11 @@ void DangerHitMapAnalyzer::calculateTileOwners() if(town->getOwner() == ai->playerID) { - vstd::amin(ourDistance, path.movementCost()); + if(ourDistance > path.movementCost()) + { + ourDistance = path.movementCost(); + ourTown = town; + } } else { @@ -181,19 +213,40 @@ void DangerHitMapAnalyzer::calculateTileOwners() if(ourDistance == enemyDistance) { - tileOwners[pos.x][pos.y][pos.z] = PlayerColor::NEUTRAL; + hitMap[pos.x][pos.y][pos.z].closestTown = nullptr; } else if(!enemyTown || ourDistance < enemyDistance) { - tileOwners[pos.x][pos.y][pos.z] = ai->playerID; + hitMap[pos.x][pos.y][pos.z].closestTown = ourTown; } else { - tileOwners[pos.x][pos.y][pos.z] = enemyTown->getOwner(); + hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown; } }); } +const std::vector & DangerHitMapAnalyzer::getTownTreats(const CGTownInstance * town) const +{ + static const std::vector empty = {}; + + auto result = townTreats.find(town->id); + + return result == townTreats.end() ? empty : result->second; +} + +PlayerColor DangerHitMapAnalyzer::getTileOwner(const int3 & tile) const +{ + auto town = hitMap[tile.x][tile.y][tile.z].closestTown; + + return town ? town->getOwner() : PlayerColor::NEUTRAL; +} + +const CGTownInstance * DangerHitMapAnalyzer::getClosestTown(const int3 & tile) const +{ + return hitMap[tile.x][tile.y][tile.z].closestTown; +} + uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const { int3 tile = path.targetTile(); diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index e96987b33..4fed77412 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -35,6 +35,8 @@ struct HitMapInfo turn = 255; hero = HeroPtr(); } + + double value() const; }; struct HitMapNode @@ -42,6 +44,8 @@ struct HitMapNode HitMapInfo maximumDanger; HitMapInfo fastestDanger; + const CGTownInstance * closestTown = nullptr; + HitMapNode() = default; void reset() @@ -51,15 +55,19 @@ struct HitMapNode } }; +struct TileOwner +{ +}; + class DangerHitMapAnalyzer { private: boost::multi_array hitMap; - boost::multi_array tileOwners; std::map> enemyHeroAccessibleObjects; bool hitMapUpToDate = false; bool tileOwnersUpToDate = false; const Nullkiller * ai; + std::map> townTreats; public: DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} @@ -72,6 +80,9 @@ public: const std::set & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); void resetTileOwners() { tileOwnersUpToDate = false; } + PlayerColor getTileOwner(const int3 & tile) const; + const CGTownInstance * getClosestTown(const int3 & tile) const; + const std::vector & getTownTreats(const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 0f971d290..229cd9121 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -54,7 +54,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->trace("Evaluating defence for %s", town->getNameTranslated()); auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - auto treats = { treatNode.maximumDanger, treatNode.fastestDanger }; + std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); + + treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); @@ -131,14 +133,24 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(treat.hero.validAndSet() && treat.turn <= 1 - && (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn) - && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)) + && (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)) { - Composition composition; + auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); + auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); - composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get())); + for(int i = 0; i < heroCapturingPaths.size(); i++) + { + AIPath & path = heroCapturingPaths[i]; + TSubgoal goal = goals[i]; - tasks.push_back(Goals::sptr(composition)); + if(!goal || goal->invalid() || !goal->isElementar()) continue; + + Composition composition; + + composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); + + tasks.push_back(Goals::sptr(composition)); + } } bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 66416df54..e88ab5928 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -12,6 +12,7 @@ #include "../Engine/Nullkiller.h" #include "../Goals/ExecuteHeroChain.h" #include "../Goals/Composition.h" +#include "../Goals/RecruitHero.h" #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" #include "GatherArmyBehavior.h" @@ -240,12 +241,22 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) logAi->trace("Found %d paths", paths.size()); #endif + bool hasMainAround = false; + + for(const AIPath & path : paths) + { + auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); + + if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + hasMainAround = true; + } + for(const AIPath & path : paths) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif - if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero) + if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1)) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Town has visiting hero."); @@ -283,25 +294,58 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources); - if(!upgrader->garrisonHero && ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN) + if(!upgrader->garrisonHero + && ( + hasMainAround + || ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN)) { - upgrade.upgradeValue += - ai->nullkiller->armyManager->howManyReinforcementsCanGet( + ArmyUpgradeInfo armyToGetOrBuy; + + armyToGetOrBuy.addArmyToGet( + ai->nullkiller->armyManager->getBestArmy( path.targetHero, path.heroArmy, - upgrader->getUpperArmy()); + upgrader->getUpperArmy())); - upgrade.upgradeValue += - ai->nullkiller->armyManager->howManyReinforcementsCanBuy( - path.heroArmy, - upgrader, - ai->nullkiller->getFreeResources(), - path.turn()); + armyToGetOrBuy.addArmyToBuy( + ai->nullkiller->armyManager->toSlotInfo( + ai->nullkiller->armyManager->getArmyAvailableToBuy( + path.heroArmy, + upgrader, + ai->nullkiller->getFreeResources(), + path.turn()))); + + upgrade.upgradeValue += armyToGetOrBuy.upgradeValue; + upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; + vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); + + auto getOrBuyArmyValue = (float)armyToGetOrBuy.upgradeValue / path.getHeroStrength(); + + if(!upgrade.upgradeValue + && armyToGetOrBuy.upgradeValue > 20000 + && ai->nullkiller->heroManager->canRecruitHero(town) + && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + { + for(auto hero : cb->getAvailableHeroes(town)) + { + auto scoutReinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(hero, town) + + ai->nullkiller->armyManager->howManyReinforcementsCanGet(hero, town); + + if(scoutReinforcement >= armyToGetOrBuy.upgradeValue + && ai->nullkiller->getFreeGold() >20000 + && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE) + { + Composition recruitHero; + + recruitHero.addNext(ArmyUpgrade(path.targetHero, town, armyToGetOrBuy)).addNext(RecruitHero(town, hero)); + } + } + } } auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); - if((armyValue < 0.1f && armyValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades + if((armyValue < 0.1f && upgrade.upgradeValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Army value is too small (%f)", armyValue); diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index bbc9dd737..fdf75ba3d 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -66,6 +66,27 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const } } + int treasureSourcesCount = 0; + + for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects()) + { + if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD)) + || obj->ID == Obj::TREASURE_CHEST + || obj->ID == Obj::CAMPFIRE + || obj->ID == Obj::WATER_WHEEL + || obj->ID ==Obj::ARTIFACT) + { + auto tile = obj->visitablePos(); + auto closestTown = ai->nullkiller->dangerHitMap->getClosestTown(tile); + + if(town == closestTown) + treasureSourcesCount++; + } + } + + if(treasureSourcesCount < 10) + continue; + if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 66ef95c7c..135f1451a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -282,18 +282,6 @@ uint64_t RewardEvaluator::getArmyReward( switch(target->ID) { - case Obj::TOWN: - { - auto town = dynamic_cast(target); - auto fortLevel = town->fortLevel(); - auto booster = isAnotherAi(town, *ai->cb) ? 1 : 2; - - if(fortLevel < CGTownInstance::CITADEL) - return town->hasFort() ? booster * 500 : 0; - else - return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000); - } - case Obj::HILL_FORT: return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; case Obj::CREATURE_BANK: @@ -440,6 +428,22 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const return std::min(ratio, 1.0f); } +uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const +{ + uint64_t result = 0; + + for(auto creatureInfo : town->creatures) + { + if(creatureInfo.second.empty()) + continue; + + auto creature = creatureInfo.second.back().toCreature(); + result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth(); + } + + return result; +} + float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const { if(!target) @@ -475,13 +479,22 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons case Obj::TOWN: { if(ai->buildAnalyzer->getDevelopmentInfo().empty()) - return 1; + return 10.0f; auto town = dynamic_cast(target); - auto fortLevel = town->fortLevel(); - auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1; - if(town->hasCapitol()) return 1; + if(town->getOwner() == ai->playerID) + { + auto armyIncome = townArmyGrowth(town); + auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; + + return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f); + } + + auto fortLevel = town->fortLevel(); + auto booster = isAnotherAi(town, *ai->cb) ? 0.3f : 0.7f; + + if(town->hasCapitol()) return booster; if(fortLevel < CGTownInstance::CITADEL) return booster * (town->hasFort() ? 0.6 : 0.4); @@ -690,23 +703,6 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin class DefendTownEvaluator : public IEvaluationContextBuilder { -private: - uint64_t townArmyIncome(const CGTownInstance * town) const - { - uint64_t result = 0; - - for(auto creatureInfo : town->creatures) - { - if(creatureInfo.second.empty()) - continue; - - auto creature = creatureInfo.second.back().toCreature(); - result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth(); - } - - return result; - } - public: virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { @@ -717,10 +713,7 @@ public: const CGTownInstance * town = defendTown.town; auto & treat = defendTown.getTreat(); - auto armyIncome = townArmyIncome(town); - auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; - - auto strategicalValue = std::sqrt(armyIncome / 60000.0f) + dailyIncome / 10000.0f; + auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town); if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) vstd::amax(evaluationContext.strategicalValue, 10.0); @@ -732,9 +725,20 @@ public: multiplier /= 1.0f + treat.turn / 5.0f; - evaluationContext.armyGrowth += armyIncome * multiplier; + if(defendTown.getTurn() > 0 && defendTown.isContrAttack()) + { + auto ourSpeed = defendTown.hero->maxMovePoints(true); + auto enemySpeed = treat.hero->maxMovePoints(true); + + if(enemySpeed > ourSpeed) multiplier *= 0.7f; + } + + auto dailyIncome = town->dailyIncome()[EGameResID::GOLD]; + auto armyGrowth = evaluationContext.evaluator.townArmyGrowth(town); + + evaluationContext.armyGrowth += armyGrowth * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier; - evaluationContext.addNonCriticalStrategicalValue(strategicalValue * multiplier); + evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 8f7f478f7..fb8085494 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -44,6 +44,7 @@ public: int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; + uint64_t townArmyGrowth(const CGTownInstance * town) const; }; struct DLL_EXPORT EvaluationContext diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.cpp b/AI/Nullkiller/Markers/ArmyUpgrade.cpp index bc475583d..0f6d41090 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.cpp +++ b/AI/Nullkiller/Markers/ArmyUpgrade.cpp @@ -28,6 +28,13 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up sethero(upgradePath.targetHero); } +ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade) + : CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue), + initialValue(targetMain->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD]) +{ + sethero(targetMain); +} + bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const { return false; diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.h b/AI/Nullkiller/Markers/ArmyUpgrade.h index df30c3588..1af41a5bf 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.h +++ b/AI/Nullkiller/Markers/ArmyUpgrade.h @@ -27,6 +27,7 @@ namespace Goals public: ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); + ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); virtual bool operator==(const ArmyUpgrade & other) const override; virtual std::string toString() const override; diff --git a/AI/Nullkiller/Markers/DefendTown.cpp b/AI/Nullkiller/Markers/DefendTown.cpp index b09952f22..096def550 100644 --- a/AI/Nullkiller/Markers/DefendTown.cpp +++ b/AI/Nullkiller/Markers/DefendTown.cpp @@ -18,8 +18,8 @@ namespace NKAI using namespace Goals; -DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath) - : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()) +DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isContrattack) + : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), contrattack(isContrattack) { settown(town); sethero(defencePath.targetHero); diff --git a/AI/Nullkiller/Markers/DefendTown.h b/AI/Nullkiller/Markers/DefendTown.h index 083f9b07f..bfe2314d9 100644 --- a/AI/Nullkiller/Markers/DefendTown.h +++ b/AI/Nullkiller/Markers/DefendTown.h @@ -24,9 +24,10 @@ namespace Goals uint64_t defenceArmyStrength; HitMapInfo treat; uint8_t turn; + bool contrattack; public: - DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath); + DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isContrattack = false); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); virtual bool operator==(const DefendTown & other) const override; @@ -37,6 +38,8 @@ namespace Goals uint64_t getDefenceStrength() const { return defenceArmyStrength; } uint8_t getTurn() const { return turn; } + + bool isContrAttack() { return contrattack; } }; } From 0fd118d3ce88baab8e2d7da4dc7d2f712d139f69 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 2 Jul 2023 10:27:30 +0300 Subject: [PATCH 070/168] NKAI: gold reward --- .../Behaviors/GatherArmyBehavior.cpp | 2 ++ AI/Nullkiller/Engine/PriorityEvaluator.cpp | 4 ++- config/ai/object-priorities.txt | 33 +++++++++++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index e88ab5928..4a2b3fc83 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -307,6 +307,8 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) path.heroArmy, upgrader->getUpperArmy())); + armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength(); + armyToGetOrBuy.addArmyToBuy( ai->nullkiller->armyManager->toSlotInfo( ai->nullkiller->armyManager->getArmyAvailableToBuy( diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 135f1451a..11efc0ec1 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -1014,6 +1014,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) + (evaluationContext.armyReward > 0 ? 1 : 0) + (evaluationContext.skillReward > 0 ? 1 : 0) + (evaluationContext.strategicalValue > 0 ? 1 : 0); + + auto goldRewardPerTurn = evaluationContext.goldReward / std::log2f(evaluationContext.movementCost * 10); double result = 0; @@ -1023,7 +1025,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) heroRoleVariable->setValue(evaluationContext.heroRole); mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); - goldRewardVariable->setValue(evaluationContext.goldReward); + goldRewardVariable->setValue(goldRewardPerTurn); armyRewardVariable->setValue(evaluationContext.armyReward); armyGrowthVariable->setValue(evaluationContext.armyGrowth); skillRewardVariable->setValue(evaluationContext.skillReward); diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index 13ec86617..acef84421 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -23,11 +23,11 @@ InputVariable: goldReward enabled: true range: 0.000 5000.000 lock-range: true - term: LOW Triangle 10.000 500.000 2000.000 - term: MEDIUM Triangle 500.000 2000.000 5000.000 - term: HIGH Ramp 2000.000 5000.000 - term: NONE Ramp 100.000 0.000 - term: LOWEST Triangle 0.000 100.000 500.000 + term: LOWEST Triangle 0.000 100.000 200.000 + term: SMALL Triangle 100.000 200.000 400.000 + term: MEDIUM Triangle 200.000 400.000 1000.000 + term: BIG Triangle 400.000 1000.000 5000.000 + term: HUGE Ramp 1000.000 5000.000 InputVariable: armyReward enabled: true range: 0.000 10000.000 @@ -97,7 +97,7 @@ InputVariable: goldPreasure range: 0.000 1.000 lock-range: false term: LOW Ramp 0.300 0.000 - term: HIGH Discrete 0.100 0.000 0.250 0.100 0.300 0.200 0.400 0.700 1.000 1.000 + term: HIGH Discrete 0.100 0.000 0.250 0.200 0.300 0.300 0.400 0.700 1.000 1.000 InputVariable: goldCost description: Action cost in gold enabled: true @@ -214,4 +214,23 @@ RuleBlock: armyReward rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM - rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL \ No newline at end of file + rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL +RuleBlock: gold + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH + rule: if goldReward is HUGE and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGHEST + rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is MEDIUM + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH + rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is MEDIUM + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is BITHIGH + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is SMALL + rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH + rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM + rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL \ No newline at end of file From 69ceee5dd6bc542c55a088ff5185859556f6e255 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 9 Jul 2023 09:56:58 +0300 Subject: [PATCH 071/168] NKAI: penalty for extra chains --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 3 ++- config/ai/object-priorities.txt | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 359286a02..37dfb7a76 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -13,6 +13,7 @@ #include "../Engine/Nullkiller.h" #include "../../../CCallback.h" #include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/GameConstants.h" namespace NKAI { @@ -306,7 +307,7 @@ std::vector ArmyManager::getArmyAvailableToBuy( int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount(); bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7; - const CGTownInstance * town = dwelling->ID == CGTownInstance::TOWN + const CGTownInstance * town = dwelling->ID == Obj::TOWN ? dynamic_cast(dwelling) : nullptr; diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index acef84421..d15d8608b 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -166,6 +166,11 @@ RuleBlock: basic rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD + rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is LONG then Value is WORST + rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is MEDIUM then Value is BAD + rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD + rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD + rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is MEDIUM then Value is BAD with 0.3 RuleBlock: strategicalValue enabled: true conjunction: AlgebraicProduct From 5bffea0aac3a4a05d0162fca4b844971d8091a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 9 Jul 2023 11:35:50 +0200 Subject: [PATCH 072/168] Use new interface --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 11efc0ec1..e3b66256c 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -727,8 +727,8 @@ public: if(defendTown.getTurn() > 0 && defendTown.isContrAttack()) { - auto ourSpeed = defendTown.hero->maxMovePoints(true); - auto enemySpeed = treat.hero->maxMovePoints(true); + auto ourSpeed = defendTown.hero->movementPointsLimit(true); + auto enemySpeed = treat.hero->movementPointsLimit(true); if(enemySpeed > ourSpeed) multiplier *= 0.7f; } @@ -894,7 +894,7 @@ public: const CGHeroInstance * dismissedHero = dismissCommand.getHero().get(); auto role = ai->heroManager->getHeroRole(dismissedHero); - auto mpLeft = dismissedHero->movement; + auto mpLeft = dismissedHero->movementPointsRemaining(); evaluationContext.movementCost += mpLeft; evaluationContext.movementCostByRole[role] += mpLeft; From e483f06e0feccf5aa62fdc4b5eeb5da45292920c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 16 Jul 2023 08:23:37 +0200 Subject: [PATCH 073/168] Remove unused variable --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index e3b66256c..cb492f7f6 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -316,8 +316,6 @@ uint64_t RewardEvaluator::getArmyGrowth( const CGHeroInstance * hero, const CCreatureSet * army) const { - const float enemyArmyEliminationRewardRatio = 0.5f; - if(!target) return 0; From 88178567b1c741db14bd207e64b31743e5132eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 16 Jul 2023 08:42:06 +0200 Subject: [PATCH 074/168] Remove unused variable --- AI/Nullkiller/Helpers/ArmyFormation.h | 1 - 1 file changed, 1 deletion(-) diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h index e5a900c01..eb7d07657 100644 --- a/AI/Nullkiller/Helpers/ArmyFormation.h +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -28,7 +28,6 @@ class DLL_EXPORT ArmyFormation { private: std::shared_ptr cb; //this is enough, but we downcast from CCallback - const Nullkiller * ai; public: ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB), ai(ai) {} From 850da65f80175c798d739027effb8d6fe0c058b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 16 Jul 2023 08:53:23 +0200 Subject: [PATCH 075/168] Another build fix --- AI/Nullkiller/Helpers/ArmyFormation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h index eb7d07657..817c6158d 100644 --- a/AI/Nullkiller/Helpers/ArmyFormation.h +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -30,7 +30,7 @@ private: std::shared_ptr cb; //this is enough, but we downcast from CCallback public: - ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB), ai(ai) {} + ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB) {} void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker); }; From db2be3ee053cbf9df6b1fd5e2967a152f85a0935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 16 Jul 2023 09:02:14 +0200 Subject: [PATCH 076/168] Anoother unused variable --- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 4a2b3fc83..80c74e8b1 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -321,8 +321,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) upgrade.upgradeCost += armyToGetOrBuy.upgradeCost; vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy); - auto getOrBuyArmyValue = (float)armyToGetOrBuy.upgradeValue / path.getHeroStrength(); - if(!upgrade.upgradeValue && armyToGetOrBuy.upgradeValue > 20000 && ai->nullkiller->heroManager->canRecruitHero(town) From 5083100d3b9ab366c14b3686d225bdbc961e849c Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Mon, 24 Jul 2023 22:19:09 +0300 Subject: [PATCH 077/168] NKA: fix accessing removed hero and heroExchangeCount --- AI/Nullkiller/Analyzers/HeroManager.cpp | 1 + AI/Nullkiller/Pathfinding/Actors.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 6afa78ec7..76da42619 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -125,6 +125,7 @@ void HeroManager::update() } std::sort(myHeroes.begin(), myHeroes.end(), scoreSort); + heroRoles.clear(); for(auto hero : myHeroes) { diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index 3fd24a222..bf176b3ec 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -134,6 +134,7 @@ void ChainActor::setBaseActor(HeroActor * base) armyCost = base->armyCost; actorAction = base->actorAction; tiCache = base->tiCache; + actorExchangeCount = base->actorExchangeCount; } void HeroActor::setupSpecialActors() From 202e13ce2e31824f41692871cc2ba0df8c67b42e Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Tue, 25 Jul 2023 17:26:35 +0300 Subject: [PATCH 078/168] NKAI: log paths scan depth --- AI/Nullkiller/Pathfinding/AIPathfinder.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp index 17cff54db..b51d19bbe 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp @@ -61,6 +61,11 @@ void AIPathfinder::updatePaths(std::map heroes storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit); storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit); + logAi->trace( + "Scout turn distance: %s, main %s", + std::to_string(pathfinderSettings.scoutTurnDistanceLimit), + std::to_string(pathfinderSettings.mainTurnDistanceLimit)); + if(pathfinderSettings.useHeroChain) { storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs); From c93bb0a5028018c02265fd03481241f02294f7f7 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Thu, 27 Jul 2023 15:58:49 +0300 Subject: [PATCH 079/168] nkai: fixes and skill rewards --- AI/Nullkiller/AIGateway.cpp | 5 + AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 48 +++- .../Behaviors/CaptureObjectsBehavior.cpp | 2 +- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 230 ++++++++++-------- .../Behaviors/GatherArmyBehavior.cpp | 23 +- AI/Nullkiller/Engine/Nullkiller.cpp | 46 +++- AI/Nullkiller/Engine/Nullkiller.h | 6 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 8 +- config/ai/object-priorities.txt | 22 +- 9 files changed, 261 insertions(+), 129 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index c3bebecbb..6301b9223 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1058,6 +1058,11 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re int count = d->creatures[i].first; CreatureID creID = d->creatures[i].second.back(); + if(!recruiter->getSlotFor(creID).validSlot()) + { + continue; + } + vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost()); if(count > 0) cb->recruitCreatures(d, recruiter, creID, count, i); diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 8318718c8..e946ecea6 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -68,19 +68,22 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo) logAi->trace("Checking other buildings"); std::vector> otherBuildings = { - {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL} + {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}, + {BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5} }; if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday) { otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE}); + otherBuildings.push_back({BuildingID::HORDE_1}); + otherBuildings.push_back({BuildingID::HORDE_2}); } for(auto & buildingSet : otherBuildings) { for(auto & buildingID : buildingSet) { - if(!developmentInfo.town->hasBuilt(buildingID)) + if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID)) { developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID)); @@ -190,12 +193,28 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( const CCreature * creature = nullptr; CreatureID baseCreatureID; + int creatureLevel = -1; + int creatureUpgrade = 0; + if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST) { - int level = toBuild - BuildingID::DWELL_FIRST; - auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN); - auto creatureID = creatures.size() > level / GameConstants::CREATURES_PER_TOWN - ? creatures.at(level / GameConstants::CREATURES_PER_TOWN) + creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN; + creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN; + } + else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR) + { + creatureLevel = townInfo->hordeLvl.at(0); + } + else if(toBuild == BuildingID::HORDE_2 || toBuild == BuildingID::HORDE_2_UPGR) + { + creatureLevel = townInfo->hordeLvl.at(1); + } + + if(creatureLevel >= 0) + { + auto creatures = townInfo->creatures.at(creatureLevel); + auto creatureID = creatures.size() > creatureUpgrade + ? creatures.at(creatureUpgrade) : creatures.front(); baseCreatureID = creatures.front(); @@ -366,12 +385,19 @@ BuildingInfo::BuildingInfo( } else { - creatureGrows = creature->getGrowth(); + if(BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST) + { + creatureGrows = creature->getGrowth(); - if(town->hasBuilt(BuildingID::CASTLE)) - creatureGrows *= 2; - else if(town->hasBuilt(BuildingID::CITADEL)) - creatureGrows += creatureGrows / 2; + if(town->hasBuilt(BuildingID::CASTLE)) + creatureGrows *= 2; + else if(town->hasBuilt(BuildingID::CITADEL)) + creatureGrows += creatureGrows / 2; + } + else + { + creatureGrows = creature->getHorde(); + } } armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows); diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index 132861467..155f45af6 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -213,7 +213,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const { captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects()); - if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL) + if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL) captureObjects(ai->nullkiller->objectClusterizer->getFarObjects()); } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 229cd9121..56bd4118c 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -49,41 +49,98 @@ Goals::TGoalVec DefenceBehavior::decompose() const return tasks; } +bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector & paths) +{ + int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); + + for(const AIPath & path : paths) + { + bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; + bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; + + if(treatIsWeak && !needToSaveGrowth) + { + if((path.exchangeCount == 1 && path.turn() < treat.turn) + || path.turn() < treat.turn - 1 + || (path.turn() < treat.turn && treat.turn >= 2)) + { +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace( + "Hero %s can eliminate danger for town %s using path %s.", + path.targetHero->getObjectName(), + town->getObjectName(), + path.toString()); +#endif + + return true; + } + } + } + + return false; +} + +void handleCounterAttack( + const CGTownInstance * town, + const HitMapInfo & treat, + const HitMapInfo & maximumDanger, + Goals::TGoalVec & tasks) +{ + if(treat.hero.validAndSet() + && treat.turn <= 1 + && (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn)) + { + auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); + auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); + + for(int i = 0; i < heroCapturingPaths.size(); i++) + { + AIPath & path = heroCapturingPaths[i]; + TSubgoal goal = goals[i]; + + if(!goal || goal->invalid() || !goal->isElementar()) continue; + + Composition composition; + + composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); + + tasks.push_back(Goals::sptr(composition)); + } + } +} + +bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks) +{ + if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) + { + logAi->trace( + "Hero %s in garrison of town %s is suposed to defend the town", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); + + return true; + } + + if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + { + logAi->trace( + "Extracting hero %s from garrison of town %s", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); + + tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); + + return true; + } + + return false; +} + void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const { logAi->trace("Evaluating defence for %s", town->getNameTranslated()); auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); - - treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there - - int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); - - if(town->garrisonHero) - { - if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) - { - logAi->trace( - "Hero %s in garrison of town %s is suposed to defend the town", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); - - return; - } - - if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) - { - logAi->trace( - "Extracting hero %s from garrison of town %s", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); - - tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - - return; - } - } if(!treatNode.fastestDanger.hero) { @@ -91,6 +148,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta return; } + + std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); + + treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there + + if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks)) + { + return; + } uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town); @@ -111,74 +177,12 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta std::to_string(treat.turn), treat.hero->getNameTranslated()); - bool treatIsUnderControl = false; + handleCounterAttack(town, treat, treatNode.maximumDanger, tasks); - for(AIPath & path : paths) + if(isTreatUnderControl(town, treat, paths)) { - if(town->visitingHero && path.targetHero == town->visitingHero.get()) - { - if(path.getHeroStrength() < town->visitingHero->getHeroStrength()) - continue; - } - else if(town->garrisonHero && path.targetHero == town->garrisonHero.get()) - { - if(path.getHeroStrength() < town->visitingHero->getHeroStrength()) - continue; - } - else - { - if(town->visitingHero) - continue; - } - - if(treat.hero.validAndSet() - && treat.turn <= 1 - && (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)) - { - auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); - auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); - - for(int i = 0; i < heroCapturingPaths.size(); i++) - { - AIPath & path = heroCapturingPaths[i]; - TSubgoal goal = goals[i]; - - if(!goal || goal->invalid() || !goal->isElementar()) continue; - - Composition composition; - - composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); - - tasks.push_back(Goals::sptr(composition)); - } - } - - bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; - bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; - - if(treatIsWeak && !needToSaveGrowth) - { - if((path.exchangeCount == 1 && path.turn() < treat.turn) - || path.turn() < treat.turn - 1 - || (path.turn() < treat.turn && treat.turn >= 2)) - { -#if NKAI_TRACE_LEVEL >= 1 - logAi->trace( - "Hero %s can eliminate danger for town %s using path %s.", - path.targetHero->getObjectName(), - town->getObjectName(), - path.toString()); -#endif - - treatIsUnderControl = true; - - break; - } - } - } - - if(treatIsUnderControl) continue; + } evaluateRecruitingHero(tasks, treat, town); @@ -205,6 +209,27 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta path.movementCost(), path.toString()); #endif + + auto townDefenseStrength = town->garrisonHero + ? town->garrisonHero->getTotalStrength() + : (town->visitingHero ? town->visitingHero->getTotalStrength() : town->getUpperArmy()->getArmyStrength()); + + if(town->visitingHero && path.targetHero == town->visitingHero.get()) + { + if(path.getHeroStrength() < townDefenseStrength) + continue; + } + else if(town->garrisonHero && path.targetHero == town->garrisonHero.get()) + { + if(path.getHeroStrength() < townDefenseStrength) + continue; + } + else + { + if(town->visitingHero) + continue; + } + if(path.turn() <= treat.turn - 2) { #if NKAI_TRACE_LEVEL >= 1 @@ -296,7 +321,20 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta composition.addNext(DefendTown(town, treat, path)); TGoalVec sequence; - if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) + if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1) + { + composition.addNext(ExchangeSwapTownHeroes(town, town->garrisonHero.get(), HeroLockedReason::DEFENCE)); + tasks.push_back(Goals::sptr(composition)); + +#if NKAI_TRACE_LEVEL >= 1 + logAi->trace("Locking hero %s in garrison of %s", + town->garrisonHero.get()->getObjectName(), + town->getObjectName()); +#endif + + continue; + } + else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) { if(town->garrisonHero) { diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 80c74e8b1..192628111 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -16,6 +16,7 @@ #include "../Markers/HeroExchange.h" #include "../Markers/ArmyUpgrade.h" #include "GatherArmyBehavior.h" +#include "CaptureObjectsBehavior.h" #include "../AIUtility.h" #include "../Goals/ExchangeSwapTownHeroes.h" @@ -235,6 +236,8 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) #endif auto paths = ai->nullkiller->pathfinder->getPathInfo(pos); + auto goals = CaptureObjectsBehavior::getVisitGoals(paths); + std::vector> waysToVisitObj; #if NKAI_TRACE_LEVEL >= 1 @@ -251,11 +254,23 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) hasMainAround = true; } - for(const AIPath & path : paths) + for(int i = 0; i < paths.size(); i++) { + auto & path = paths[i]; + auto visitGoal = goals[i]; + #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); #endif + + if(visitGoal->invalid()) + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Ignore path. Not valid way."); +#endif + continue; + } + if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1)) { #if NKAI_TRACE_LEVEL >= 2 @@ -370,11 +385,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) if(isSafe) { - ExecuteHeroChain newWay(path, upgrader); - - newWay.closestWayRatio = 1; - - tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(newWay))); + tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(visitGoal))); } } diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 55976d638..8aadd804b 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -118,7 +118,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi void Nullkiller::resetAiState() { lockedResources = TResources(); - scanDepth = ScanDepth::FULL; + scanDepth = ScanDepth::MAIN_FULL; playerID = ai->playerID; lockedHeroes.clear(); dangerHitMap->reset(); @@ -158,11 +158,15 @@ void Nullkiller::updateAiState(int pass, bool fast) PathfinderSettings cfg; cfg.useHeroChain = useHeroChain; - cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT; - if(scanDepth != ScanDepth::FULL) + if(scanDepth == ScanDepth::SMALL) { - cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT * ((int)scanDepth + 1); + cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT; + } + + if(scanDepth != ScanDepth::ALL_FULL) + { + cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT; } boost::this_thread::interruption_point(); @@ -233,8 +237,8 @@ void Nullkiller::makeTurn() updateAiState(i); Goals::TTask bestTask = taskptr(Goals::Invalid()); - - do + + for(;i <= MAXPASS; i++) { Goals::TTaskVec fastTasks = { choseBestTask(sptr(BuyArmyBehavior()), 1), @@ -248,7 +252,11 @@ void Nullkiller::makeTurn() executeTask(bestTask); updateAiState(i, true); } - } while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY); + else + { + break; + } + } Goals::TTaskVec bestTasks = { bestTask, @@ -267,7 +275,6 @@ void Nullkiller::makeTurn() bestTask = choseBestTask(bestTasks); HeroPtr hero = bestTask->getHero(); - HeroRole heroRole = HeroRole::MAIN; if(hero.validAndSet()) @@ -276,20 +283,39 @@ void Nullkiller::makeTurn() if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1) useHeroChain = false; + // TODO: better to check turn distance here instead of priority if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY) - && scanDepth == ScanDepth::FULL) + && scanDepth == ScanDepth::MAIN_FULL) { useHeroChain = false; scanDepth = ScanDepth::SMALL; logAi->trace( - "Goal %s has too low priority %f so increasing scan depth", + "Goal %s has low priority %f so decreasing scan depth to gain performance.", bestTask->toString(), bestTask->priority); } if(bestTask->priority < MIN_PRIORITY) { + auto heroes = cb->getHeroesInfo(); + auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool + { + return h->movementPointsRemaining() > 100; + }); + + if(hasMp && scanDepth != ScanDepth::ALL_FULL) + { + logAi->trace( + "Goal %s has too low priority %f so increasing scan depth to full.", + bestTask->toString(), + bestTask->priority); + + scanDepth = ScanDepth::ALL_FULL; + useHeroChain = false; + continue; + } + logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString()); return; diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index d47e634ea..36f3504fd 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -40,9 +40,11 @@ enum class HeroLockedReason enum class ScanDepth { - FULL = 0, + MAIN_FULL = 0, - SMALL = 1 + SMALL = 1, + + ALL_FULL = 2 }; class Nullkiller diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index cb492f7f6..b45f7e4d5 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -915,6 +915,7 @@ public: evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD]; + evaluationContext.closestWayRatio = 1; if(bi.creatureID != CreatureID::NONE) { @@ -938,7 +939,12 @@ public: evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f); evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2; } - else + else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) + { + evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); + } + + if(evaluationContext.goldReward) { auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure(); diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index d15d8608b..a5b3fa5ed 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -5,7 +5,7 @@ InputVariable: mainTurnDistance enabled: true range: 0.000 10.000 lock-range: true - term: LOWEST Ramp 0.250 0.000 + term: LOWEST Ramp 0.400 0.000 term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000 term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000 term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800 @@ -238,4 +238,22 @@ RuleBlock: gold rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is SMALL rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM - rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL \ No newline at end of file + rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL +RuleBlock: skill reward + enabled: true + conjunction: AlgebraicProduct + disjunction: AlgebraicSum + implication: AlgebraicProduct + activation: General + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH + rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LONG and fear is not HIGH then Value is SMALL + rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH \ No newline at end of file From 6490c65490ed8ea3073b65e727827ac301049a00 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Fri, 28 Jul 2023 14:17:01 +0300 Subject: [PATCH 080/168] nkai: fix freezes --- AI/Nullkiller/Analyzers/HeroManager.cpp | 25 ++++++++ AI/Nullkiller/Analyzers/HeroManager.h | 2 + AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 70 +++++++++------------ AI/Nullkiller/Engine/PriorityEvaluator.cpp | 11 ++-- config/ai/object-priorities.txt | 15 ++++- 5 files changed, 78 insertions(+), 45 deletions(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 76da42619..44b1df23a 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -229,6 +229,31 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const return nullptr; } +const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const +{ + const CGHeroInstance * weakestHero = nullptr; + auto myHeroes = ai->cb->getHeroesInfo(); + + for(auto existingHero : myHeroes) + { + if(ai->isHeroLocked(existingHero) + || existingHero->getArmyStrength() >armyLimit + || getHeroRole(existingHero) == HeroRole::MAIN + || existingHero->movementPointsRemaining() + || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) + { + continue; + } + + if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) + { + weakestHero = existingHero; + } + } + + return weakestHero; +} + SecondarySkillScoreMap::SecondarySkillScoreMap(std::map scoreMap) :scoreMap(scoreMap) { diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 84da85b98..1009fd31e 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -33,6 +33,7 @@ public: virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0; virtual bool heroCapReached() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0; + virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0; }; class DLL_EXPORT ISecondarySkillRule @@ -74,6 +75,7 @@ public: bool canRecruitHero(const CGTownInstance * t = nullptr) const override; bool heroCapReached() const override; const CGHeroInstance * findHeroWithGrail() const override; + const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override; private: float evaluateFightingStrength(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 56bd4118c..3e1ee23f2 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -121,16 +121,31 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa return true; } - if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + if(!town->visitingHero) { - logAi->trace( - "Extracting hero %s from garrison of town %s", - town->garrisonHero->getNameTranslated(), - town->getNameTranslated()); + if(cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER) + { + logAi->trace( + "Extracting hero %s from garrison of town %s", + town->garrisonHero->getNameTranslated(), + town->getNameTranslated()); - tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); + tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); - return true; + return true; + } + else if(ai->nullkiller->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) + { + auto armyDismissLimit = 1000; + auto heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(armyDismissLimit); + + if(heroToDismiss) + { + tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); + + return true; + } + } } return false; @@ -141,14 +156,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->trace("Evaluating defence for %s", town->getNameTranslated()); auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - - if(!treatNode.fastestDanger.hero) - { - logAi->trace("No treat found for town %s", town->getNameTranslated()); - - return; - } - std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there @@ -157,6 +164,13 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { return; } + + if(!treatNode.fastestDanger.hero) + { + logAi->trace("No treat found for town %s", town->getNameTranslated()); + + return; + } uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town); @@ -224,11 +238,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta if(path.getHeroStrength() < townDefenseStrength) continue; } - else - { - if(town->visitingHero) - continue; - } if(path.turn() <= treat.turn - 2) { @@ -440,27 +449,10 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM } else if(ai->nullkiller->heroManager->heroCapReached()) { - const CGHeroInstance * weakestHero = nullptr; + heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); - for(auto existingHero : myHeroes) - { - if(ai->nullkiller->isHeroLocked(existingHero) - || existingHero->getArmyStrength() > hero->getArmyStrength() - || ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN - || existingHero->movementPointsRemaining() - || existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1)) - continue; - - if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength()) - { - weakestHero = existingHero; - } - } - - if(!weakestHero) + if(!heroToDismiss) continue; - - heroToDismiss = weakestHero; } TGoalVec sequence; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index b45f7e4d5..ac6c9618c 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -592,7 +592,6 @@ int32_t getArmyCost(const CArmedInstance * army) return value; } -/// Gets aproximated reward in gold. Daily income is multiplied by 5 int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const { if(!target) @@ -713,9 +712,6 @@ public: auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town); - if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) - vstd::amax(evaluationContext.strategicalValue, 10.0); - float multiplier = 1; if(treat.turn < defendTown.getTurn()) @@ -736,7 +732,12 @@ public: evaluationContext.armyGrowth += armyGrowth * multiplier; evaluationContext.goldReward += dailyIncome * 5 * multiplier; - evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + + if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1) + vstd::amax(evaluationContext.strategicalValue, 2.5f * multiplier * strategicalValue); + else + evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); + vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); } diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index a5b3fa5ed..d5439ee18 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -171,6 +171,10 @@ RuleBlock: basic rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is MEDIUM then Value is BAD with 0.3 + rule: if heroRole is SCOUT and fear is HIGH then Value is BAD with 0.8 + rule: if heroRole is SCOUT and fear is MEDIUM then Value is BAD with 0.5 + rule: if heroRole is MAIN and fear is HIGH then Value is BAD with 0.5 + rule: if heroRole is MAIN and fear is MEDIUM then Value is BAD with 0.2 RuleBlock: strategicalValue enabled: true conjunction: AlgebraicProduct @@ -205,7 +209,8 @@ RuleBlock: strategicalValue rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7 - rule: if strategicalValue is CRITICAL then Value is CRITICAL + rule: if strategicalValue is CRITICAL and heroRole is MAIN then Value is CRITICAL + rule: if strategicalValue is CRITICAL and heroRole is SCOUT then Value is CRITICAL with 0.7 RuleBlock: armyReward enabled: true conjunction: AlgebraicProduct @@ -220,6 +225,14 @@ RuleBlock: armyReward rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.7 + rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH with 0.7 + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH + rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM + rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL RuleBlock: gold enabled: true conjunction: AlgebraicProduct From fb7477047a3a78a584b9c17c853cc8d5fee62e20 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 29 Jul 2023 18:54:20 +0300 Subject: [PATCH 081/168] NKAI: loosen gold presure on build system. --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 2 +- AI/Nullkiller/Analyzers/HeroManager.cpp | 2 +- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 2 +- AI/Nullkiller/Engine/Nullkiller.cpp | 5 +++-- AI/Nullkiller/Engine/Nullkiller.h | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 10 ++++++---- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 6 +++++- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index e946ecea6..690437447 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -166,7 +166,7 @@ void BuildAnalyzer::update() } else { - goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 10000.0f + goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f + (float)armyCost[EGameResID::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 44b1df23a..ac5fff683 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -236,7 +236,7 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co for(auto existingHero : myHeroes) { - if(ai->isHeroLocked(existingHero) + if(ai->isHeroLocked(existingHero) && ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE || existingHero->getArmyStrength() >armyLimit || getHeroRole(existingHero) == HeroRole::MAIN || existingHero->movementPointsRemaining() diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 192628111..c626a36d2 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -360,7 +360,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength(); - if((armyValue < 0.1f && upgrade.upgradeValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades + if((armyValue < 0.25f && upgrade.upgradeValue < 40000) || upgrade.upgradeValue < 2000) // avoid small upgrades { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Ignore path. Army value is too small (%f)", armyValue); diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 8aadd804b..66b28ca8e 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -134,6 +134,9 @@ void Nullkiller::updateAiState(int pass, bool fast) activeHero = nullptr; setTargetObject(-1); + decomposer->reset(); + buildAnalyzer->update(); + if(!fast) { memory->removeInvisibleObjects(cb.get()); @@ -179,8 +182,6 @@ void Nullkiller::updateAiState(int pass, bool fast) } armyManager->update(); - buildAnalyzer->update(); - decomposer->reset(); logAi->debug("AI state updated in %ld", timeElapsed(start)); } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 36f3504fd..04578ebec 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -23,7 +23,7 @@ namespace NKAI { -const float MAX_GOLD_PEASURE = 0.3f; +const float MAX_GOLD_PEASURE = 0.6f; const float MIN_PRIORITY = 0.01f; const float SMALL_SCAN_MIN_PRIORITY = 0.4f; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index ac6c9618c..bbf931355 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -420,10 +420,10 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const return 0; float ratio = dailyIncome[resType] == 0 - ? (float)requiredResources[resType] / 50.0f - : (float)requiredResources[resType] / dailyIncome[resType] / 50.0f; + ? (float)requiredResources[resType] / 10.0f + : (float)requiredResources[resType] / dailyIncome[resType] / 20.0f; - return std::min(ratio, 1.0f); + return std::min(ratio, 2.0f); } uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const @@ -954,7 +954,9 @@ public: if(bi.notEnoughRes && bi.prerequisitesCount == 1) { - evaluationContext.strategicalValue /= 2; + evaluationContext.strategicalValue /= 3; + evaluationContext.movementCostByRole[evaluationContext.heroRole] += 5; + evaluationContext.turn += 5; } } }; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 2fbd50a72..4ff2e4ef9 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -879,8 +879,12 @@ void AINodeStorage::setHeroes(std::map heroes) for(auto & hero : heroes) { // do not allow our own heroes in garrison to act on map - if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison && ai->isHeroLocked(hero.first)) + if(hero.first->getOwner() == ai->playerID + && hero.first->inTownGarrison + && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached())) + { continue; + } uint64_t mask = FirstActorMask << actors.size(); auto actor = std::make_shared(hero.first, hero.second, mask, ai); From f1a9ae99ee5920a943ba8c71db069280c18f5e84 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 30 Jul 2023 11:33:52 +0300 Subject: [PATCH 082/168] NKAI: various behavior fixes, undo max_gold_preasure --- AI/BattleAI/BattleAI.cpp | 2 +- AI/Nullkiller/AIGateway.cpp | 29 ++++------------ AI/Nullkiller/AIGateway.h | 1 - AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 2 +- AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 7 +++- AI/Nullkiller/Engine/FuzzyHelper.cpp | 10 +++--- AI/Nullkiller/Engine/Nullkiller.h | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 34 +++++++++++++------ AI/Nullkiller/Pathfinding/AINodeStorage.h | 4 +-- config/ai/object-priorities.txt | 6 +++- 10 files changed, 49 insertions(+), 48 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index d3c4ba13c..b3986de94 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -863,7 +863,7 @@ std::optional CBattleAI::considerFleeingOrSurrendering() bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); - if(!bs.canFlee || !bs.canSurrender) + if(!bs.canFlee && !bs.canSurrender) { return std::nullopt; } diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 6301b9223..a8911d3b6 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -779,25 +779,21 @@ void AIGateway::makeTurn() boost::shared_lock gsLock(CGameState::mutex); setThreadName("AIGateway::makeTurn"); + cb->sendMessage("vcmieagles"); + + retrieveVisitableObjs(); + if(cb->getDate(Date::DAY_OF_WEEK) == 1) { - std::vector objs; - retrieveVisitableObjs(objs, true); - - for(const CGObjectInstance * obj : objs) + for(const CGObjectInstance * obj : nullkiller->memory->visitableObjs) { if(isWeeklyRevisitable(obj)) { - addVisitableObj(obj); nullkiller->memory->markObjectUnvisited(obj); } } } - cb->sendMessage("vcmieagles"); - - retrieveVisitableObjs(); - #if NKAI_TRACE_LEVEL == 0 try { @@ -1106,26 +1102,13 @@ void AIGateway::waitTillFree() status.waitTillFree(); } -void AIGateway::retrieveVisitableObjs(std::vector & out, bool includeOwned) const -{ - foreach_tile_pos([&](const int3 & pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) - { - if(includeOwned || obj->tempOwner != playerID) - out.push_back(obj); - } - }); -} - void AIGateway::retrieveVisitableObjs() { foreach_tile_pos([&](const int3 & pos) { for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) { - if(obj->tempOwner != playerID) - addVisitableObj(obj); + addVisitableObj(obj); } }); } diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index cab6ce797..3a9c23b31 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -195,7 +195,6 @@ public: void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it - void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; void retrieveVisitableObjs(); virtual std::vector getFlaggedObjects() const; diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 690437447..359ccc1ca 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -167,7 +167,7 @@ void BuildAnalyzer::update() else { goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f - + (float)armyCost[EGameResID::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); + + (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f); } logAi->trace("Gold preasure: %f", goldPreasure); diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 3aee0dcbf..5e2b41977 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -227,7 +227,12 @@ void ObjectClusterizer::clusterize() auto obj = objs[i]; if(!shouldVisitObject(obj)) - return; + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); +#endif + continue; + } #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index f9ac898dc..2757cb35a 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -150,17 +150,15 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) case Obj::MINE: case Obj::ABANDONED_MINE: case Obj::PANDORAS_BOX: - { - const CArmedInstance * a = dynamic_cast(obj); - return a->getArmyStrength(); - } case Obj::CRYPT: //crypt case Obj::CREATURE_BANK: //crebank case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship - // case Obj::PYRAMID: - return estimateBankDanger(dynamic_cast(obj)); + { + const CArmedInstance * a = dynamic_cast(obj); + return a->getArmyStrength(); + } case Obj::PYRAMID: { if(obj->subID == 0) diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 04578ebec..36f3504fd 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -23,7 +23,7 @@ namespace NKAI { -const float MAX_GOLD_PEASURE = 0.6f; +const float MAX_GOLD_PEASURE = 0.3f; const float MIN_PRIORITY = 0.01f; const float SMALL_SCAN_MIN_PRIORITY = 0.4f; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index bbf931355..5d2e1685a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -162,11 +162,11 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero { result += (c.data.type->getAIValue() * c.data.count) * c.chance; } - else + /*else { //we will need to discard the weakest stack result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance; - } + }*/ } result /= 100; //divide by total chance @@ -277,6 +277,8 @@ uint64_t RewardEvaluator::getArmyReward( { const float enemyArmyEliminationRewardRatio = 0.5f; + auto relations = ai->cb->getPlayerRelations(target->tempOwner, ai->playerID); + if(!target) return 0; @@ -301,7 +303,7 @@ uint64_t RewardEvaluator::getArmyReward( case Obj::DRAGON_UTOPIA: return 10000; case Obj::HERO: - return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + return relations == PlayerRelations::ENEMIES ? enemyArmyEliminationRewardRatio * dynamic_cast(target)->getArmyStrength() : 0; case Obj::PANDORAS_BOX: @@ -319,6 +321,11 @@ uint64_t RewardEvaluator::getArmyGrowth( if(!target) return 0; + auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner); + + if(relations != PlayerRelations::ENEMIES) + return 0; + switch(target->ID) { case Obj::TOWN: @@ -542,15 +549,18 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::GARDEN_OF_REVELATION: case Obj::MARLETTO_TOWER: case Obj::MERCENARY_CAMP: - case Obj::SHRINE_OF_MAGIC_GESTURE: - case Obj::SHRINE_OF_MAGIC_INCANTATION: case Obj::TREE_OF_KNOWLEDGE: return 1; case Obj::LEARNING_STONE: return 1.0f / std::sqrt(hero->level); case Obj::ARENA: - case Obj::SHRINE_OF_MAGIC_THOUGHT: return 2; + case Obj::SHRINE_OF_MAGIC_INCANTATION: + return 0.2f; + case Obj::SHRINE_OF_MAGIC_GESTURE: + return 0.3f; + case Obj::SHRINE_OF_MAGIC_THOUGHT: + return 0.5f; case Obj::LIBRARY_OF_ENLIGHTENMENT: return 8; case Obj::WITCH_HUT: @@ -597,6 +607,8 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG if(!target) return 0; + auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner); + const int dailyIncomeMultiplier = 5; const float enemyArmyEliminationGoldRewardRatio = 0.2f; const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2; @@ -637,7 +649,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG //Objectively saves us 2500 to hire hero return GameConstants::HERO_GOLD_COST; case Obj::HERO: - return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES + return relations == PlayerRelations::ENEMIES ? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast(target)) : 0; default: @@ -788,7 +800,7 @@ public: if(heroRole == HeroRole::MAIN) evaluationContext.heroRole = heroRole; - if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES) + if (target) { evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero); evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold); @@ -1022,7 +1034,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) + (evaluationContext.skillReward > 0 ? 1 : 0) + (evaluationContext.strategicalValue > 0 ? 1 : 0); - auto goldRewardPerTurn = evaluationContext.goldReward / std::log2f(evaluationContext.movementCost * 10); + float goldRewardPerTurn = evaluationContext.goldReward / std::log2f(2 + evaluationContext.movementCost * 10); double result = 0; @@ -1055,13 +1067,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task) } #if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %d, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", + logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f", task->toString(), evaluationContext.armyLossPersentage, (int)evaluationContext.turn, evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::SCOUT], - evaluationContext.goldReward, + goldRewardPerTurn, evaluationContext.goldCost, evaluationContext.armyReward, evaluationContext.danger, diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 02364ad11..c127f294b 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -11,7 +11,7 @@ #pragma once #define NKAI_PATHFINDER_TRACE_LEVEL 0 -#define NKAI_TRACE_LEVEL 2 +#define NKAI_TRACE_LEVEL 0 #include "../../../lib/pathfinder/CGPathNode.h" #include "../../../lib/pathfinder/INodeStorage.h" @@ -258,7 +258,7 @@ public: { double ratio = (double)danger / (armyValue * hero->getFightingStrength()); - return (uint64_t)(armyValue * ratio * ratio * ratio); + return (uint64_t)(armyValue * ratio * ratio); } STRONG_INLINE diff --git a/config/ai/object-priorities.txt b/config/ai/object-priorities.txt index d5439ee18..1989bc3c7 100644 --- a/config/ai/object-priorities.txt +++ b/config/ai/object-priorities.txt @@ -170,7 +170,6 @@ RuleBlock: basic rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is MEDIUM then Value is BAD rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD - rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is MEDIUM then Value is BAD with 0.3 rule: if heroRole is SCOUT and fear is HIGH then Value is BAD with 0.8 rule: if heroRole is SCOUT and fear is MEDIUM then Value is BAD with 0.5 rule: if heroRole is MAIN and fear is HIGH then Value is BAD with 0.5 @@ -252,6 +251,11 @@ RuleBlock: gold rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL + rule: if goldReward is LOWEST then Value is SMALL with 0.1 + rule: if goldReward is SMALL then Value is SMALL with 0.2 + rule: if goldReward is MEDIUM then Value is SMALL with 0.5 + rule: if goldReward is BIG then Value is SMALL + rule: if goldReward is HUGE then Value is BITHIGH RuleBlock: skill reward enabled: true conjunction: AlgebraicProduct From ec0596f3ddcfb8a92bf37b6b5daf254601a84d71 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 30 Jul 2023 12:21:47 +0300 Subject: [PATCH 083/168] NKAI: fix error message can not take away last stack --- AI/Nullkiller/AIGateway.cpp | 13 +++++++++++++ AI/Nullkiller/Analyzers/HeroManager.cpp | 2 +- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 5 +++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index a8911d3b6..8be0aa310 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -867,6 +867,19 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source); + for(auto army : armies) + { + // move first stack at first slot if empty to avoid can not take away last creature + if(!army->hasStackAtSlot(SlotID(0)) && army->stacksCount() > 0) + { + cb->mergeOrSwapStacks( + army, + army, + SlotID(0), + army->Slots().begin()->first); + } + } + //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot { diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index ac5fff683..b896ab728 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -236,7 +236,7 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co for(auto existingHero : myHeroes) { - if(ai->isHeroLocked(existingHero) && ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE + if(ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE || existingHero->getArmyStrength() >armyLimit || getHeroRole(existingHero) == HeroRole::MAIN || existingHero->movementPointsRemaining() diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index c626a36d2..f5eb28c79 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -119,10 +119,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her HeroExchange heroExchange(hero, path); - float armyValue = (float)heroExchange.getReinforcementArmyStrength() / hero->getArmyStrength(); + uint64_t armyValue = heroExchange.getReinforcementArmyStrength(); + float armyRatio = (float)armyValue / hero->getArmyStrength(); // avoid transferring very small amount of army - if(armyValue < 0.1f && armyValue < 20000) + if((armyRatio < 0.1f && armyValue < 20000) || armyValue < 500) { #if NKAI_TRACE_LEVEL >= 2 logAi->trace("Army value is too small."); From ccfc6f57169387e85cd5bfd5fbe3557e2f1fce13 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 30 Jul 2023 18:02:56 +0300 Subject: [PATCH 084/168] NKAI: increase towns priority, buy heroes more often --- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp | 6 ++++-- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h | 4 ---- AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp | 6 +++--- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 14 ++++++++------ AI/Nullkiller/Markers/DefendTown.cpp | 4 ++-- AI/Nullkiller/Markers/DefendTown.h | 6 +++--- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 7bd330137..5b2638341 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -148,17 +148,19 @@ void DangerHitMapAnalyzer::calculateTileOwners() std::map heroTownMap; PathfinderSettings pathfinderSettings; - pathfinderSettings.mainTurnDistanceLimit = 3; + pathfinderSettings.mainTurnDistanceLimit = 5; auto addTownHero = [&](const CGTownInstance * town) { auto townHero = new CGHeroInstance(); CRandomGenerator rng; + auto visitablePos = town->visitablePos(); - townHero->pos = town->pos; + townHero->pos = visitablePos; townHero->setOwner(ai->playerID); // lets avoid having multiple colors townHero->initHero(rng, static_cast(0)); townHero->initObj(rng); + townHero->pos = townHero->convertFromVisitablePos(visitablePos); heroTownMap[townHero] = town; townHeroes[townHero] = HeroRole::MAIN; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 4fed77412..79c56c2c4 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -55,10 +55,6 @@ struct HitMapNode } }; -struct TileOwner -{ -}; - class DangerHitMapAnalyzer { private: diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index fdf75ba3d..581c725e6 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -70,10 +70,10 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects()) { - if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD)) + if((obj->ID == Obj::RESOURCE) || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE - || obj->ID == Obj::WATER_WHEEL + || isWeeklyRevisitable(obj) || obj->ID ==Obj::ARTIFACT) { auto tile = obj->visitablePos(); @@ -84,7 +84,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const } } - if(treasureSourcesCount < 10) + if(treasureSourcesCount < 5) continue; if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 5d2e1685a..6e3e5f754 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -108,7 +108,8 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons auto town = cb->getTown(target->id); auto fortLevel = town->fortLevel(); - if(town->hasCapitol()) return booster * 2000; + if(town->hasCapitol()) + return booster * 2000; // probably well developed town will have city hall if(fortLevel == CGTownInstance::CASTLE) return booster * 750; @@ -497,14 +498,15 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons } auto fortLevel = town->fortLevel(); - auto booster = isAnotherAi(town, *ai->cb) ? 0.3f : 0.7f; + auto booster = isAnotherAi(town, *ai->cb) ? 0.4f : 1.0f; - if(town->hasCapitol()) return booster; + if(town->hasCapitol()) + return booster * 1.5; if(fortLevel < CGTownInstance::CITADEL) - return booster * (town->hasFort() ? 0.6 : 0.4); + return booster * (town->hasFort() ? 1.0 : 0.8); else - return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8); + return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2); } case Obj::HERO: @@ -731,7 +733,7 @@ public: multiplier /= 1.0f + treat.turn / 5.0f; - if(defendTown.getTurn() > 0 && defendTown.isContrAttack()) + if(defendTown.getTurn() > 0 && defendTown.isCounterAttack()) { auto ourSpeed = defendTown.hero->movementPointsLimit(true); auto enemySpeed = treat.hero->movementPointsLimit(true); diff --git a/AI/Nullkiller/Markers/DefendTown.cpp b/AI/Nullkiller/Markers/DefendTown.cpp index 096def550..dd7d08e25 100644 --- a/AI/Nullkiller/Markers/DefendTown.cpp +++ b/AI/Nullkiller/Markers/DefendTown.cpp @@ -18,8 +18,8 @@ namespace NKAI using namespace Goals; -DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isContrattack) - : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), contrattack(isContrattack) +DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack) + : CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack) { settown(town); sethero(defencePath.targetHero); diff --git a/AI/Nullkiller/Markers/DefendTown.h b/AI/Nullkiller/Markers/DefendTown.h index bfe2314d9..34b8b3427 100644 --- a/AI/Nullkiller/Markers/DefendTown.h +++ b/AI/Nullkiller/Markers/DefendTown.h @@ -24,10 +24,10 @@ namespace Goals uint64_t defenceArmyStrength; HitMapInfo treat; uint8_t turn; - bool contrattack; + bool counterattack; public: - DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isContrattack = false); + DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); virtual bool operator==(const DefendTown & other) const override; @@ -39,7 +39,7 @@ namespace Goals uint8_t getTurn() const { return turn; } - bool isContrAttack() { return contrattack; } + bool isCounterAttack() { return counterattack; } }; } From 4c0aae6fbdcf6a55bc54cf1e9aa1a9512e1b5217 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Mon, 31 Jul 2023 22:00:22 +0300 Subject: [PATCH 085/168] NKAI: fix crash for specific map --- AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 5b2638341..efc4bde8e 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -156,11 +156,10 @@ void DangerHitMapAnalyzer::calculateTileOwners() CRandomGenerator rng; auto visitablePos = town->visitablePos(); - townHero->pos = visitablePos; townHero->setOwner(ai->playerID); // lets avoid having multiple colors townHero->initHero(rng, static_cast(0)); - townHero->initObj(rng); townHero->pos = townHero->convertFromVisitablePos(visitablePos); + townHero->initObj(rng); heroTownMap[townHero] = town; townHeroes[townHero] = HeroRole::MAIN; From 8a81b3013fa7a01e56b5f7f74f4ef7bf1c25d93e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 31 Jul 2023 22:06:54 +0300 Subject: [PATCH 086/168] Fixed transferring of artifacts from backpack in campaigns --- lib/gameState/CGameStateCampaign.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index d271f887c..0d8729a94 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -118,28 +118,35 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorartifactsInBackpack.size(); - for (size_t i = 0; i < totalArts; i++ ) + auto const & checkAndRemoveArtifact = [&](const ArtifactPosition & artifactPosition ) { - auto artifactPosition = ArtifactPosition((si32)i); if(artifactPosition == ArtifactPosition::SPELLBOOK) - continue; // do not handle spellbook this way + return; // do not handle spellbook this way const ArtSlotInfo *info = hero->getSlot(artifactPosition); if(!info) - continue; + return; // TODO: why would there be nullptr artifacts? const CArtifactInstance *art = info->artifact; if(!art) - continue; + return; bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId()); ArtifactLocation al(hero, artifactPosition); if(!takeable && !al.getSlot()->locked) //don't try removing locked artifacts -> it crashes #1719 al.removeArtifact(); - } + }; + + // process on copy - removal of artifact will invalidate container + auto artifactsWorn = hero->artifactsWorn; + for (auto const & art : artifactsWorn) + checkAndRemoveArtifact(art.first); + + // process in reverse - removal of artifact will shift all artifacts after this one + for(int slotNumber = hero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) + checkAndRemoveArtifact(ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); } } From 791eb1a37baf0308cde02917cedf120552b999a2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 1 Aug 2023 14:47:17 +0300 Subject: [PATCH 087/168] Added workaround for running game with outdated extras mod from 1.2 --- client/battle/BattleWindow.cpp | 2 +- config/widgets/{battleWindow.json => battleWindow2.json} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename config/widgets/{battleWindow.json => battleWindow2.json} (100%) diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index ac99ee935..36c430272 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -51,7 +51,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); - const JsonNode config(ResourceID("config/widgets/BattleWindow.json")); + const JsonNode config(ResourceID("config/widgets/BattleWindow2.json")); addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); diff --git a/config/widgets/battleWindow.json b/config/widgets/battleWindow2.json similarity index 100% rename from config/widgets/battleWindow.json rename to config/widgets/battleWindow2.json From 3b238ff15e9d92adec6ced7cf60e9ada1f1ef651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Tue, 1 Aug 2023 09:54:35 +0200 Subject: [PATCH 088/168] Fix weekly visitable check --- AI/Nullkiller/AIUtility.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 296c47780..e6962673e 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -323,13 +323,9 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj) if(dynamic_cast(obj)) return true; - if(dynamic_cast(obj)) //banks tend to respawn often in mods - return true; switch(obj->ID) { - case Obj::STABLES: - case Obj::MAGIC_WELL: case Obj::HILL_FORT: return true; case Obj::BORDER_GATE: From f6247164adf58bc3252280e2ca6dbf7739511fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Tue, 1 Aug 2023 18:51:33 +0200 Subject: [PATCH 089/168] Fix crash, fix invisible boat blocking the tile --- lib/NetPacksLib.cpp | 10 ++++------ lib/gameState/CGameState.cpp | 3 +-- lib/mapObjects/CGHeroInstance.cpp | 18 +++++++++++++++--- lib/mapObjects/CGHeroInstance.h | 1 + server/HeroPoolProcessor.cpp | 9 +++++---- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index b1810d343..db8a2dd8d 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1382,9 +1382,8 @@ void HeroRecruited::applyGs(CGameState * gs) const auto * boat = dynamic_cast(obj); if (boat) { - h->boat = boat; - h->attachTo(*boat); - boat->hero = h; + gs->map->removeBlockVisTiles(boat); + h->attachToBoat(boat); } } @@ -1419,9 +1418,8 @@ void GiveHero::applyGs(CGameState * gs) const auto * boat = dynamic_cast(obj); if (boat) { - h->boat = boat; - h->attachTo(*boat); - boat->hero = h; + gs->map->removeBlockVisTiles(boat); + h->attachToBoat(boat); } } diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 780ed3cb2..782f9d815 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -857,8 +857,7 @@ void CGameState::initHeroes() map->objects.emplace_back(boat); map->addBlockVisTiles(boat); - boat->hero = hero; - hero->boat = boat; + hero->attachToBoat(boat); } } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 5bacb0c73..80fac1523 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -484,9 +484,12 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const if (cb->gameState()->map->getTile(boatPos).isWater()) { smp.val = movementPointsLimit(false); - //Create a new boat for hero - cb->createObject(boatPos, Obj::BOAT, getBoatType().getNum()); - boatId = cb->getTopObj(boatPos)->id; + if (!boat) + { + //Create a new boat for hero + cb->createObject(boatPos, Obj::BOAT, getBoatType().getNum()); + boatId = cb->getTopObj(boatPos)->id; + } } else { @@ -1119,6 +1122,15 @@ int CGHeroInstance::maxSpellLevel() const return std::min(GameConstants::SPELL_LEVELS, valOfBonuses(Selector::type()(BonusType::MAX_LEARNABLE_SPELL_LEVEL))); } +void CGHeroInstance::attachToBoat(CGBoat* newBoat) +{ + assert(newBoat); + boat = newBoat; + attachTo(const_cast(*boat)); + const_cast(boat)->hero = this; +} + + void CGHeroInstance::deserializationFix() { artDeserializationFix(this); diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 891b1d502..a2ca01d81 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -282,6 +282,7 @@ public: void getCastDescription(const spells::Spell * spell, const std::vector & attacked, MetaString & text) const override; void spendMana(ServerCallback * server, const int spellCost) const override; + void attachToBoat(CGBoat* newBoat); void boatDeserializationFix(); void deserializationFix(); diff --git a/server/HeroPoolProcessor.cpp b/server/HeroPoolProcessor.cpp index 9923123dd..2d50595b0 100644 --- a/server/HeroPoolProcessor.cpp +++ b/server/HeroPoolProcessor.cpp @@ -237,18 +237,19 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy gameHandler->complain("Hero is not available for hiring!"); return false; } + const auto targetPos = mapObject->visitablePos(); HeroRecruited hr; hr.tid = mapObject->id; hr.hid = recruitedHero->subID; hr.player = player; - hr.tile = recruitedHero->convertFromVisitablePos(mapObject->visitablePos()); - if(gameHandler->getTile(hr.tile)->isWater()) + hr.tile = recruitedHero->convertFromVisitablePos(targetPos ); + if(gameHandler->getTile(hr.tile)->isWater() && !recruitedHero->boat) { //Create a new boat for hero - gameHandler->createObject(mapObject->visitablePos(), Obj::BOAT, recruitedHero->getBoatType().getNum()); + gameHandler->createObject(targetPos , Obj::BOAT, recruitedHero->getBoatType().getNum()); - hr.boatId = gameHandler->getTopObj(hr.tile)->id; + hr.boatId = gameHandler->getTopObj(targetPos)->id; } // apply netpack -> this will remove hired hero from pool From 250b27ba4680df68ad0836a2261a3ae8f5459d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Wed, 2 Aug 2023 17:19:23 +0200 Subject: [PATCH 090/168] Fix crash due to missing boat for neutrla heroes --- lib/CTownHandler.cpp | 8 ++++++-- lib/CTownHandler.h | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 78c849e92..f756c79de 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -110,7 +110,11 @@ void CBuilding::addNewBonus(const std::shared_ptr & b, BonusList & bonusL CFaction::~CFaction() { - delete town; + if (town) + { + delete town; + town = nullptr; + } } int32_t CFaction::getIndex() const @@ -1029,7 +1033,7 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode faction->creatureBg120 = source["creatureBackground"]["120px"].String(); faction->creatureBg130 = source["creatureBackground"]["130px"].String(); - faction->boatType = EBoatId::NONE; + faction->boatType = EBoatId::CASTLE; //Do not crash if (!source["boat"].isNull()) { VLC->modh->identifiers.requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID) diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 3acbf9a70..66aec6196 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -207,7 +207,7 @@ public: /// Boat that will be used by town shipyard (if any) /// and for placing heroes directly on boat (in map editor, water prisons & taverns) - BoatId boatType; + BoatId boatType = BoatId(EBoatId::CASTLE); CTown * town = nullptr; //NOTE: can be null From 3f7e5fcfc344408b97fb0fb029d64ab17acc977d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 2 Aug 2023 21:23:27 +0300 Subject: [PATCH 091/168] Fix possible assertion failure on resolution change --- client/adventureMap/AdventureMapInterface.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 0f7ef98e2..45ea69a63 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -822,6 +822,15 @@ void AdventureMapInterface::hotkeyZoom(int delta) void AdventureMapInterface::onScreenResize() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + // remember our activation state and reactive after reconstruction + // since othervice activate() calls for created elements will bypass virtual dispatch + // and will call directly CIntObject::activate() instead of dispatching virtual function call + bool widgetActive = isActive(); + + if (widgetActive) + deactivate(); + widget.reset(); pos.x = pos.y = 0; pos.w = GH.screenDimensions().x; @@ -838,4 +847,7 @@ void AdventureMapInterface::onScreenResize() widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy()); adjustActiveness(); + + if (widgetActive) + activate(); } From 8122acd2dc0f086cf449dbbf4216399e83008ba9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 1 Aug 2023 19:10:34 +0300 Subject: [PATCH 092/168] Updated changelog with final changes --- ChangeLog.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 2bc0da2e2..24edce517 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,4 @@ # 1.2.1 -> 1.3.0 -(unreleased) ### GENERAL: * Implemented automatic interface scaling to any resolution supported by monitor @@ -17,6 +16,7 @@ * Added H3:SOD cheat codes as alternative to vcmi cheats * Fixed several possible crashes caused by autocombat activation * Fixed artifact lock icon in localized versions of the game +* Fixed possible crash on changing hardware cursor ### TOUCHSCREEN SUPPORT: * VCMI will now properly recognizes touch screen input @@ -47,6 +47,10 @@ ### AI PLAYER: * Fixed potential crash on accessing market (VCAI) * Fixed potentially infinite turns (VCAI) +* Reworked object prioritizing +* Improved town defense against enemy heroes +* Improved town building (mage guild and horde) +* Various behavior fixes ### GAME MECHANICS * Hero retreating after end of 7th turn will now correctly appear in tavern @@ -72,6 +76,7 @@ * Game will now play correct music track on scenario selection window * Dracon woll now correctly start without spellbook in Dragon Slayer campaign * Fixed frequent crash on moving to next scenario during campaign +* Fixed inability to dismiss heroes on maps with "capture town" victory condition ### RANDOM MAP GENERATOR: * Improved zone placement, shape and connections @@ -86,6 +91,7 @@ * Support for "wide" connections * Support for new "fictive" and "repulsive" connections * RMG will now run faster, utilizing many CPU cores +* Removed random seed number from random map description ### INTERFACE: * Adventure map is now scalable and can be used with any resolution without mods @@ -105,6 +111,8 @@ * Last symbol of entered cheat/chat message will no longer trigger hotkey * Right-clicking map name in scenario selection will now show file name * Right-clicking save game in save/load screen will now show file name and creation date +* Right-clicking in town fort window will now show creature information popup +* Implemented pasting from clipboard (Ctrl+V) for text input ### BATTLES: * Implemented Tower moat (Land Mines) @@ -139,6 +147,7 @@ * Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance * MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses * Configurable objects can now be translated +* Fixed loading of custom battlefield identifiers for map objects # 1.2.0 -> 1.2.1 From 8ef7956204694258344deb4689ec17d4df78ab49 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 1 Aug 2023 19:10:44 +0300 Subject: [PATCH 093/168] Version bump for Android --- android/vcmi-app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 81554e855..f8d5a4edd 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 31 - versionCode 1201 - versionName "1.2.1" + versionCode 1300 + versionName "1.3.0" setProperty("archivesBaseName", "vcmi") } From 799bd9612abbb3d141d0e53b5420fc83d3e75c52 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 1 Aug 2023 19:11:06 +0300 Subject: [PATCH 094/168] Set release date to August, 4th --- debian/changelog | 2 +- launcher/eu.vcmi.VCMI.metainfo.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index f9f61f980..25cc2af65 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,7 +2,7 @@ vcmi (1.3.0) jammy; urgency=medium * New upstream release - -- Ivan Savenko Sat, 01 Jul 2023 16:00:00 +0200 + -- Ivan Savenko Fri, 04 Aug 2023 16:00:00 +0200 vcmi (1.2.1) jammy; urgency=medium diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 233758bff..aac246f10 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -51,7 +51,7 @@ StrategyGame - + From 67ea948e81d84d6de1f7152693256d1d0d8b6faf Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 1 Aug 2023 20:28:51 +0300 Subject: [PATCH 095/168] Update download counter --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 38f4003b5..18ca646d1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) [![Coverity Scan Build Status](https://scan.coverity.com/projects/vcmi/badge.svg)](https://scan.coverity.com/projects/vcmi) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.2.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.2.1) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.2.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.2.0) +[![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/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. From 44fae4945da4e7d15b3e514c848fdd51cd69073c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Wed, 2 Aug 2023 20:41:29 +0200 Subject: [PATCH 096/168] Update distances in all zones adjacent to object --- lib/rmg/modificators/ObjectManager.cpp | 28 ++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 5b8136a01..46f001c90 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -216,6 +216,7 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg::Object & obj, si32 min_dist, bool isGuarded, bool onlyStraight, OptimizeType optimizer) const { + RecursiveLock lock(externalAccessMutex); return placeAndConnectObject(searchArea, obj, [this, min_dist, &obj](const int3 & tile) { auto ti = map.getTileInfo(tile); @@ -455,8 +456,31 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD map.setOccupied(i, ETileType::BLOCKED); } - if(updateDistance) - updateDistances(object); + if (updateDistance) + { + //Update distances in every adjacent zone in case of wide connection + + std::set adjacentZones; + auto objectArea = object.getArea(); + objectArea.unite(objectArea.getBorderOutside()); + + for (auto tile : objectArea.getTilesVector()) + { + if (map.isOnMap(tile)) + { + adjacentZones.insert(map.getZoneID(tile)); + } + } + + for (auto id : adjacentZones) + { + auto manager = map.getZones().at(id)->getModificator(); + if (manager) + { + manager->updateDistances(object); + } + } + } for(auto * instance : object.instances()) { From 915283e1268fe0bed1f2038d4b0f2e038be5c13a Mon Sep 17 00:00:00 2001 From: Dydzio Date: Wed, 2 Aug 2023 21:10:09 +0200 Subject: [PATCH 097/168] Update polish translations --- Mods/vcmi/config/vcmi/english.json | 4 +- Mods/vcmi/config/vcmi/polish.json | 20 ++++- launcher/translation/polish.ts | 16 ++-- mapeditor/translation/polish.ts | 122 ++++++++++++++--------------- 4 files changed, 90 insertions(+), 72 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 26eecc0ba..edf089678 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -84,8 +84,8 @@ "vcmi.systemOptions.longTouchMenu.entry" : "%d milliseconds", "vcmi.systemOptions.framerateButton.hover" : "Show FPS", "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window", - "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Wibracje urządzenia", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Wibracje urządzenia}\n\nWłącz wibracje na urządzeniu dotykowym", "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index 9abf585bb..f9ecd2f71 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -30,6 +30,13 @@ "vcmi.capitalColors.6" : "Jasnoniebieski", "vcmi.capitalColors.7" : "Różowy", + "vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia", + "vcmi.radialWheel.showUnitInformation" : "Pokaż informacje o stworzeniu", + "vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie", + "vcmi.radialWheel.splitUnitEqually" : "Podziel stworzenia równo", + "vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii", + "vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca", + "vcmi.mainMenu.tutorialNotImplemented" : "Przepraszamy, trening nie został jeszcze zaimplementowany\n", "vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n", "vcmi.mainMenu.serverConnecting" : "Łączenie...", @@ -39,6 +46,9 @@ "vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP", "vcmi.mainMenu.playerName" : "Gracz", + "vcmi.lobby.filename" : "Nazwa pliku", + "vcmi.lobby.creationDate" : "Data utworzenia", + "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", "vcmi.server.errors.modsIncompatibility" : "Następujące mody są wymagane do wczytania gry:", "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", @@ -74,6 +84,8 @@ "vcmi.systemOptions.longTouchMenu.entry" : "%d milisekund", "vcmi.systemOptions.framerateButton.hover" : "Pokaż FPS", "vcmi.systemOptions.framerateButton.help" : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs", "vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym", "vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.", @@ -85,6 +97,10 @@ "vcmi.adventureOptions.showGrid.help" : "{Pokaż siatkę}\n\n Włącza siatkę pokazującą brzegi pól mapy przygody.", "vcmi.adventureOptions.borderScroll.hover" : "Przewijanie na brzegu mapy", "vcmi.adventureOptions.borderScroll.help" : "{Przewijanie na brzegu mapy}\n\nPrzewijanie mapy przygody gdy kursor najeżdża na brzeg okna gry. Może być wyłączone poprzez przytrzymanie klawisza CTRL.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Zarządzanie armią w panelu informacyjnym", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie mapy lewym kliknięciem", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nGdy włączone, umożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", @@ -111,10 +127,12 @@ "vcmi.battleOptions.movementHighlightOnHover.help": "{Pokaż możliwości ruchu po najechaniu}\n\nPodświetla zasięg ruchu jednostki gdy najedziesz na nią myszą.", "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Pokaż limit zasięgu dla strzelców", "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Pokaż limit zasięgu dla strzelców po najechaniu}\n\nPokazuje limity zasięgu jednostki strzeleckiej gdy najedziesz na nią myszą.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Pokaż trwale statystyki bohaterów", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Pokaż trwale statystyki bohaterów}\n\nWłącza trwałe okna statystyk bohaterów pokazujące umiejętności pierwszorzędne i punkty magii.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.", - "vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo", "vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).", "vcmi.battleWindow.damageEstimation.ranged" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE).", diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 72bfa264a..d72e7c62e 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -472,7 +472,7 @@ Autosave limit (0 = off) - + Limit autozapisów (0 = brak) @@ -487,12 +487,12 @@ Autosave prefix - + Przedrostek autozapisu empty = map name prefix - + puste = przedrostek z nazwy mapy @@ -708,7 +708,7 @@ Heroes III: HD Edition nie jest obecnie wspierane! Interface Improvements - + Ulepszenia interfejsu @@ -723,7 +723,7 @@ Heroes III: HD Edition nie jest obecnie wspierane! Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - + Zainstaluj modyfikację, która dostarcza różne ulepszenia interfejsu takie jak lepszy ekran ustawień mapy losowej lub wybieralne akcje w bitwach @@ -916,17 +916,17 @@ Heroes III: HD Edition nie jest obecnie wspierane! Other (East European) - + Inne (Wschodnioeuropejski) Other (Cyrillic Script) - + Inne (Cyrylica) Other (West European) - + Inne (Zachodnioeuropejski) diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index c47231394..ef642f63b 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -137,7 +137,7 @@ Save as... - Zapisz jako + Zapisz jako... @@ -287,27 +287,27 @@ Export as... - + Eksportuj jako... Confirmation - + Potwierdzenie Unsaved changes will be lost, are you sure? - + Niezapisane zmiany zostaną utracone, jesteś pewny? Failed to open map - + Nie udało się otworzyć mapy Cannot open map from this folder - + Nie można otworzyć mapy z tego folderu @@ -344,22 +344,22 @@ No objects selected - + Brak wybranych obiektów This operation is irreversible. Do you want to continue? - + Ta operacja jest nieodwracalna. Czy chcesz kontynuować? Errors occured. %1 objects were not updated - + Wystąpiły błędy. %1 obiektów nie zostało zaktualizowanych Save to image - + Zapisz jako obraz @@ -387,7 +387,7 @@ Limit maximum heroes level - + Ogranicz maksymalny poziom bohaterów @@ -397,47 +397,47 @@ Mods - + Modyfikacje Mandatory mods for playing this map - + Obowiązkowe modyfikacje do uruchomienia tej mapy Mod name - + Nazwa modyfikacji Version - + Wersja Automatic assignment - + Automatyczne przypisanie Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - + Wybierz wymagane modyfikacje bazując na obiektach umeszczonych na mapie. Ta metoda może stworzyć problemy jeśli masz własne nagrody, garnizony itp. z modyfikacji Map objects mods - + Mody od nowych obiektów mapy Set all mods having a game content as mandatory - + Ustaw wszystkie modyfikacje mające nową elementy gry jako obowiązkowe Full content mods - + Mody od złożonej zawartości @@ -513,7 +513,7 @@ No special victory - Bez specjalnych warunków zwycięstwa + Bez specjalnych warunków zwycięstwa @@ -523,57 +523,57 @@ Hire creatures - Zdobądź stworzenia + Zdobądź stworzenia Accumulate resources - Zbierz zasoby + Zbierz zasoby Construct building - Zbuduj budynek + Zbuduj budynek Capture town - Zdobądź miasto + Zdobądź miasto Defeat hero - Pokonaj bohatera + Pokonaj bohatera Transport artifact - Przenieś artefakt + Przenieś artefakt No special loss - Bez specjalnych warunków porażki + Bez specjalnych warunków porażki Lose castle - Utrata miasta + Utrata miasta Lose hero - Utrata bohatera + Utrata bohatera Time expired - Upłynięcie czasu + Upłynięcie czasu Days without town - Dni bez miasta + Dni bez miasta @@ -581,7 +581,7 @@ Can't place object - + Nie można umieścić obiektu @@ -621,7 +621,7 @@ Color - + Kolor @@ -659,7 +659,7 @@ 1 - 1 + 1 @@ -716,37 +716,37 @@ Map is not loaded - + Mapa nie została wczytana No factions allowed for player %1 - + Brak dozwolonych frakcji dla gracza %1 No players allowed to play this map - + Żaden gracz nie jest dozwolony do rozegrania tej mapy Map is allowed for one player and cannot be started - + Mapa jest dozwolona dla jednego gracza i nie może być rozpoczęta No human players allowed to play this map - + Żaden gracz ludzki nie został dozwolony by rozegrać tą mapę Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Obiekt z armią %1 jest nie do oflagowania, lecz musi mieć właściciela neutralnego lub gracza Object %1 is assigned to non-playable player %2 - + Obiekt %1 został przypisany do niegrywalnego gracza %2 @@ -756,72 +756,72 @@ Prison %1 must be a NEUTRAL - + Więzienie %1 musi być neutralne Hero %1 must have an owner - + Bohater %1 musi mieć właściciela Hero %1 is prohibited by map settings - + Bohater %1 jest zabroniony przez ustawienia mapy Hero %1 has duplicate on map - + Bohater %1 posiada duplikat na mapie Hero %1 has an empty type and must be removed - + Bohater %1 jest pustego typu i musi zostać usunięty Spell scroll %1 is prohibited by map settings - + Zwój z zaklęciem %1 jest zabroniony przez ustawienia mapy Spell scroll %1 doesn't have instance assigned and must be removed - + Zwój z zaklęciem %1 nie ma przypisanej instancji i musi zostać usunięty Artifact %1 is prohibited by map settings - + Artefakt %1 jest zabroniony przez ustawienia mapy Player %1 doesn't have any starting town - + Gracz %1 nie ma żadnego startowego miasta Map name is not specified - + Nazwa mapy nie została ustawiona Map description is not specified - + Opis mapy nie został ustawiony Map contains object from mod "%1", but doesn't require it - + Mapa zawiera obiekt z modyfikacji %1 ale nie wymaga tej modyfikacji Exception occurs during validation: %1 - + Wystąpił wyjątek podczas walidacji: %1 Unknown exception occurs during validation - + Wystąpił nieznane wyjątek podczas walidacji @@ -907,12 +907,12 @@ Human teams - + Sojusze ludzkie Computer teams - + Sojusze komputerowe @@ -978,17 +978,17 @@ No template - + Brak szablonu No template for parameters scecified. Random map cannot be generated. - + Brak szablonu dla wybranych parametrów. Mapa losowa nie może zostać wygenerowana. RMG failure - + Niepowodzenie generatora map losowych From 852c681e9bf1848314ea78fd13e837a1c0be1adf Mon Sep 17 00:00:00 2001 From: Dydzio Date: Wed, 2 Aug 2023 21:12:58 +0200 Subject: [PATCH 098/168] Minor fix --- Mods/vcmi/config/vcmi/english.json | 4 ++-- Mods/vcmi/config/vcmi/polish.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index edf089678..26eecc0ba 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -84,8 +84,8 @@ "vcmi.systemOptions.longTouchMenu.entry" : "%d milliseconds", "vcmi.systemOptions.framerateButton.hover" : "Show FPS", "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window", - "vcmi.systemOptions.hapticFeedbackButton.hover" : "Wibracje urządzenia", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Wibracje urządzenia}\n\nWłącz wibracje na urządzeniu dotykowym", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs", "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index f9ecd2f71..c6a5f4231 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -84,8 +84,8 @@ "vcmi.systemOptions.longTouchMenu.entry" : "%d milisekund", "vcmi.systemOptions.framerateButton.hover" : "Pokaż FPS", "vcmi.systemOptions.framerateButton.help" : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.", - "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Wibracje urządzenia", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Wibracje urządzenia}\n\nWłącz wibracje na urządzeniu dotykowym", "vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym", "vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.", From 3213800097817e83951cff7bd6c13bcd44e11f9e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 3 Aug 2023 14:20:54 +0300 Subject: [PATCH 099/168] Fix assertion failure on teleport cast --- client/battle/BattleInterface.cpp | 2 ++ client/battle/BattleStacksController.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 1f49a635c..6420cdc77 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -445,6 +445,8 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero)); }); } + + // animations will be executed by spell effects } void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 4321d3dac..4abc8b6ed 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -499,7 +499,7 @@ void BattleStacksController::stacksAreAttacked(std::vector at void BattleStacksController::stackTeleported(const CStack *stack, std::vector destHex, int distance) { assert(destHex.size() > 0); - owner.checkForAnimations(); + //owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) ); From 57d7dc97bf6474d2c3066781a42d1639ee6d728f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 3 Aug 2023 14:21:15 +0300 Subject: [PATCH 100/168] Fix assertion failure on swiping during spellcast --- client/gui/CursorHandler.h | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/client/gui/CursorHandler.h b/client/gui/CursorHandler.h index 4ef4fe8a5..531594b3a 100644 --- a/client/gui/CursorHandler.h +++ b/client/gui/CursorHandler.h @@ -31,8 +31,6 @@ namespace Cursor }; enum class Combat { - INVALID = -1, - BLOCKED = 0, MOVE = 1, FLY = 2, @@ -157,12 +155,16 @@ public: template Index get() { - assert((std::is_same::value )|| type != Cursor::Type::DEFAULT ); - assert((std::is_same::value )|| type != Cursor::Type::ADVENTURE ); - assert((std::is_same::value )|| type != Cursor::Type::COMBAT ); - assert((std::is_same::value )|| type != Cursor::Type::SPELLBOOK ); + bool typeValid = true; - return static_cast(frame); + typeValid &= (std::is_same::value )|| type != Cursor::Type::DEFAULT; + typeValid &= (std::is_same::value )|| type != Cursor::Type::ADVENTURE; + typeValid &= (std::is_same::value )|| type != Cursor::Type::COMBAT; + typeValid &= (std::is_same::value )|| type != Cursor::Type::SPELLBOOK; + + if (typeValid) + return static_cast(frame); + return Index::POINTER; } Point getPivotOffsetDefault(size_t index); From 45cbc4d5508b9cf530ef6ee3d6255cd708c51ff3 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 3 Aug 2023 15:25:36 +0300 Subject: [PATCH 101/168] Added workaround for mingw CI builds failing due to TBB bug --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index fb193b230..0cf681b8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,6 +279,10 @@ if(MINGW OR MSVC) endif(MSVC) if(MINGW) + + # Temporary (?) workaround for failing builds on MinGW CI due to bug in TBB + set(CMAKE_CXX_EXTENSIONS ON) + set(SYSTEM_LIBS ${SYSTEM_LIBS} ole32 oleaut32 ws2_32 mswsock dbghelp bcrypt) # Check for iconv (may be needed for Boost.Locale) From 6e7a02702db714f8c6b4e7141f4fc146473582f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Fri, 4 Aug 2023 05:59:39 +0200 Subject: [PATCH 102/168] Fix crash in naval combat --- server/CGameHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 9f677a237..5b6385ac0 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2122,7 +2122,7 @@ void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const BattleField terType = gs->battleGetBattlefieldType(tile, getRandomGenerator()); if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) - terType = BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield", "ship_to_ship")); + terType = BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield.ship_to_ship")); //send info about battles BattleStart bs; From b9729241f1018b4a07f880b319da32f94bf6409b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 4 Aug 2023 11:18:51 +0300 Subject: [PATCH 103/168] Version bump for Android to reupload build --- android/vcmi-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index f8d5a4edd..87ded4332 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,7 +10,7 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 31 - versionCode 1300 + versionCode 1301 versionName "1.3.0" setProperty("archivesBaseName", "vcmi") } From ed927de12ed349afc9f60c3e7461b85a8264271a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 4 Aug 2023 21:41:11 +0300 Subject: [PATCH 104/168] Added more informative error messages for battle action errors --- server/NetPacksServer.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index d6812f1ef..f2f37e4a4 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -282,24 +282,24 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) { const BattleInfo * b = gs.curB; if(!b) - gh.throwNotAllowedAction(&pack); + gh.throwAndComplain(&pack, "Can not make action - there is no battle ongoing!"); if(b->tacticDistance) { if(pack.ba.actionType != EActionType::WALK && pack.ba.actionType != EActionType::END_TACTIC_PHASE && pack.ba.actionType != EActionType::RETREAT && pack.ba.actionType != EActionType::SURRENDER) - gh.throwNotAllowedAction(&pack); + gh.throwAndComplain(&pack, "Can not make actions while in tactics mode!"); if(!vstd::contains(gh.connections[b->sides[b->tacticsSide].color], pack.c)) - gh.throwNotAllowedAction(&pack); + gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); } else { auto active = b->battleActiveUnit(); if(!active) - gh.throwNotAllowedAction(&pack); + gh.throwAndComplain(&pack, "No active unit in battle!"); auto unitOwner = b->battleGetOwner(active); if(!vstd::contains(gh.connections[unitOwner], pack.c)) - gh.throwNotAllowedAction(&pack); + gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); } result = gh.makeBattleAction(pack.ba); From 77dcac19a0f6fe7ea0204e3a2311816eb1970b8d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 12:48:04 +0300 Subject: [PATCH 105/168] Version bump for vcmi core mod --- Mods/vcmi/mod.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index 945927a8b..0a0702f0b 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -78,7 +78,7 @@ ] }, - "version" : "1.2", + "version" : "1.3", "author" : "VCMI Team", "contact" : "http://forum.vcmi.eu/index.php", "modType" : "Graphical", From dd17896082f78f52c390a10542c6da8798b7effa Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 12:49:02 +0300 Subject: [PATCH 106/168] Fix crash on bonus parsing failure (outdated mods?) --- lib/JsonNode.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 41edb62ab..bae2f077c 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -829,7 +829,13 @@ std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) auto b = std::make_shared(); if (!parseBonus(ability, b.get())) { - return nullptr; + // caller code can not handle this case and presumes that returned bonus is always valid + logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson()); + + b->type = BonusType::NONE; + assert(0); // or throw? Game *should* work with dummy bonus + + return b; } return b; } From 041236837b9d72c53be92425e15a92346fa16be4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 12:49:38 +0300 Subject: [PATCH 107/168] Possible fix for crash from Android crash reporting --- lib/CHeroHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index bb688b39d..173678c17 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -770,7 +770,7 @@ std::vector CHeroHandler::getDefaultAllowed() const for(const CHero * hero : objects) { - allowedHeroes.push_back(!hero->special); + allowedHeroes.push_back(hero && !hero->special); } return allowedHeroes; From 8d5aff4c122ed7c553abab9ee832f56e64cba156 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 12:58:25 +0300 Subject: [PATCH 108/168] Attempt of a blind fix for mod load crash on Android launcher --- .../src/main/java/eu/vcmi/vcmi/ActivityMods.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java index df42c711d..4f3cc0582 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java @@ -182,9 +182,16 @@ public class ActivityMods extends ActivityWithToolbar public void onSuccess(ServerResponse> response) { Log.i(this, "Initialized mods repo"); - mModContainer.updateFromRepo(response.mContent); - mModsAdapter.updateModsList(mModContainer.submods()); - mProgress.setVisibility(View.GONE); + if (mModContainer == null) + { + handleNoData(); + } + else + { + mModContainer.updateFromRepo(response.mContent); + mModsAdapter.updateModsList(mModContainer.submods()); + mProgress.setVisibility(View.GONE); + } } @Override From e6fde112827c5ad103001279eed2a8327737c398 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 13:35:07 +0300 Subject: [PATCH 109/168] Another attempt to make debug symbols show up on Android --- CMakePresets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index d3f03fa5c..39c752e94 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -240,7 +240,7 @@ "default-release" ], "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "RelWithDebInfo" } } ], From 2cf323a6c176bc214eca5662c8b36ba5f393df6b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 13:35:24 +0300 Subject: [PATCH 110/168] Version bump for Android build --- android/vcmi-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 87ded4332..1d818d14a 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,7 +10,7 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 31 - versionCode 1301 + versionCode 1302 versionName "1.3.0" setProperty("archivesBaseName", "vcmi") } From 59c599cbf9ed603e9d27b788c5e266c7cab12a41 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 13:35:58 +0300 Subject: [PATCH 111/168] Reverted FPS limit change on mobile systems --- config/schemas/settings.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 4d98720ed..20699af25 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -201,8 +201,6 @@ }, "targetfps" : { "type" : "number", - "defaultIOS" : 30, // reduce battery usage - "defaultAndroid" : 30, // reduce battery usage "default" : 60 } } From 00f38046047f8204bda81b40ecc69ffe59a11fcb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 13:39:08 +0300 Subject: [PATCH 112/168] Workaround for crash? --- client/NetPacksClient.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index dea5b8a55..c2f89316f 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -95,6 +95,14 @@ void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) template void callBattleInterfaceIfPresentForBothSides(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) { + assert(cl.gameState()->curB); + + if (!cl.gameState()->curB) + { + logGlobal->error("Attempt to call battle interface without ongoing battle!"); + return; + } + callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[0].color, ptr, std::forward(args)...); callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[1].color, ptr, std::forward(args)...); if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) From ab4fb2a2280b86d8c769445f988b4db572d9854e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 14:25:57 +0300 Subject: [PATCH 113/168] Prefer composition over inheritance --- client/widgets/MiscWidgets.cpp | 41 +++++++++++++++++++--------------- client/widgets/MiscWidgets.h | 8 ++++--- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 0fdc7f18e..6a973bb18 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -18,6 +18,7 @@ #include "../CPlayerInterface.h" #include "../CGameInfo.h" #include "../widgets/TextControls.h" +#include "../widgets/CGarrisonInt.h" #include "../windows/CCastleInterface.h" #include "../windows/InfoWindows.h" #include "../render/Canvas.h" @@ -303,28 +304,30 @@ CHeroTooltip::CHeroTooltip(Point pos, const CGHeroInstance * hero): init(InfoAboutHero(hero, InfoAboutHero::EInfoLevel::DETAILED)); } -CInteractableHeroTooltip::CInteractableHeroTooltip(Point pos, const CGHeroInstance * hero): - CGarrisonInt(pos + Point(0, 73), 4, Point(0, 0), hero, nullptr, true, true, CGarrisonInt::ESlotsLayout::REVERSED_TWO_ROWS) +CInteractableHeroTooltip::CInteractableHeroTooltip(Point pos, const CGHeroInstance * hero) { init(InfoAboutHero(hero, InfoAboutHero::EInfoLevel::DETAILED)); + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + garrison = std::make_shared(pos + Point(0, 73), 4, Point(0, 0), hero, nullptr, true, true, CGarrisonInt::ESlotsLayout::REVERSED_TWO_ROWS); } void CInteractableHeroTooltip::init(const InfoAboutHero & hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - portrait = std::make_shared("PortraitsLarge", hero.portrait, 0, 3, 2-73); - title = std::make_shared(66, 2-73, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, hero.name); + portrait = std::make_shared("PortraitsLarge", hero.portrait, 0, 3, 2); + title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, hero.name); if(hero.details) { for(size_t i = 0; i < hero.details->primskills.size(); i++) - labels.push_back(std::make_shared(75 + 28 * (int)i, 58-73, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, + labels.push_back(std::make_shared(75 + 28 * (int)i, 58, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(hero.details->primskills[i]))); - labels.push_back(std::make_shared(158, 98-73, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(hero.details->mana))); + labels.push_back(std::make_shared(158, 98, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(hero.details->mana))); - morale = std::make_shared("IMRL22", hero.details->morale + 3, 0, 5, 74-73); - luck = std::make_shared("ILCK22", hero.details->luck + 3, 0, 5, 91-73); + morale = std::make_shared("IMRL22", hero.details->morale + 3, 0, 5, 74); + luck = std::make_shared("ILCK22", hero.details->luck + 3, 0, 5, 91); } } @@ -383,9 +386,11 @@ CTownTooltip::CTownTooltip(Point pos, const CGTownInstance * town) } CInteractableTownTooltip::CInteractableTownTooltip(Point pos, const CGTownInstance * town) - : CGarrisonInt(pos + Point(0, 73), 4, Point(0, 0), town->getUpperArmy(), nullptr, true, true, CGarrisonInt::ESlotsLayout::REVERSED_TWO_ROWS) { init(InfoAboutTown(town, true)); + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + garrison = std::make_shared(pos + Point(0, 73), 4, Point(0, 0), town->getUpperArmy(), nullptr, true, true, CGarrisonInt::ESlotsLayout::REVERSED_TWO_ROWS); } void CInteractableTownTooltip::init(const InfoAboutTown & town) @@ -395,37 +400,37 @@ void CInteractableTownTooltip::init(const InfoAboutTown & town) //order of icons in def: fort, citadel, castle, no fort size_t fortIndex = town.fortLevel ? town.fortLevel - 1 : 3; - fort = std::make_shared("ITMCLS", fortIndex, 0, 105, 31-73); + fort = std::make_shared("ITMCLS", fortIndex, 0, 105, 31); assert(town.tType); size_t iconIndex = town.tType->clientInfo.icons[town.fortLevel > 0][town.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; - build = std::make_shared("itpt", iconIndex, 0, 3, 2-73); - title = std::make_shared(66, 2-73, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, town.name); + build = std::make_shared("itpt", iconIndex, 0, 3, 2); + title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, town.name); if(town.details) { - hall = std::make_shared("ITMTLS", town.details->hallLevel, 0, 67, 31-73); + hall = std::make_shared("ITMTLS", town.details->hallLevel, 0, 67, 31); if(town.details->goldIncome) { - income = std::make_shared(157, 58-73, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, + income = std::make_shared(157, 58, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(town.details->goldIncome)); } if(town.details->garrisonedHero) //garrisoned hero icon - garrisonedHero = std::make_shared("TOWNQKGH", 149, 76-73); + garrisonedHero = std::make_shared("TOWNQKGH", 149, 76); if(town.details->customRes)//silo is built { if(town.tType->primaryRes == EGameResID::WOOD_AND_ORE )// wood & ore { - res1 = std::make_shared("SMALRES", GameResID(EGameResID::WOOD), 0, 7, 75-73); - res2 = std::make_shared("SMALRES", GameResID(EGameResID::ORE), 0, 7, 88-73); + res1 = std::make_shared("SMALRES", GameResID(EGameResID::WOOD), 0, 7, 75); + res2 = std::make_shared("SMALRES", GameResID(EGameResID::ORE), 0, 7, 88); } else { - res1 = std::make_shared("SMALRES", town.tType->primaryRes, 0, 7, 81-73); + res1 = std::make_shared("SMALRES", town.tType->primaryRes, 0, 7, 81); } } } diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 56d31b643..f2e316cb3 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -10,7 +10,6 @@ #pragma once #include "../gui/CIntObject.h" -#include "CGarrisonInt.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,6 +21,7 @@ class AFactionMember; VCMI_LIB_NAMESPACE_END class CLabel; +class CGarrisonInt; class CCreatureAnim; class CComponent; class CAnimImage; @@ -82,13 +82,14 @@ public: }; /// Class for HD mod-like interactable infobox tooltip. Does not have any background! -class CInteractableHeroTooltip : public CGarrisonInt +class CInteractableHeroTooltip : public CIntObject { std::shared_ptr title; std::shared_ptr portrait; std::vector> labels; std::shared_ptr morale; std::shared_ptr luck; + std::shared_ptr garrison; void init(const InfoAboutHero & hero); public: @@ -115,7 +116,7 @@ public: }; /// Class for HD mod-like interactable infobox tooltip. Does not have any background! -class CInteractableTownTooltip : public CGarrisonInt +class CInteractableTownTooltip : public CIntObject { std::shared_ptr title; std::shared_ptr fort; @@ -125,6 +126,7 @@ class CInteractableTownTooltip : public CGarrisonInt std::shared_ptr garrisonedHero; std::shared_ptr res1; std::shared_ptr res2; + std::shared_ptr garrison; void init(const InfoAboutTown & town); public: From c8ec0d54193036d342825dad77b425d796341cb4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 14:26:54 +0300 Subject: [PATCH 114/168] Added caching for creature icons in garrisons More complete version of caching will be done for 1.4 Fixes freezes on hero movement when hota mod is used --- client/render/Graphics.cpp | 14 ++++++++++++++ client/render/Graphics.h | 4 ++++ client/widgets/CGarrisonInt.cpp | 4 ++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index 440653255..bb9e385e9 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -299,3 +299,17 @@ void Graphics::initializeImageLists() addImageListEntries(CGI->spells()); addImageListEntries(CGI->skills()); } + +std::shared_ptr Graphics::getAnimation(const std::string & path) +{ + ResourceID animationPath(path, EResType::ANIMATION); + + if (cachedAnimations.count(animationPath.getName()) != 0) + return cachedAnimations.at(animationPath.getName()); + + auto newAnimation = std::make_shared(animationPath.getName()); + + newAnimation->preload(); + cachedAnimations[animationPath.getName()] = newAnimation; + return newAnimation; +} diff --git a/client/render/Graphics.h b/client/render/Graphics.h index 50ee21be2..380c539fb 100644 --- a/client/render/Graphics.h +++ b/client/render/Graphics.h @@ -47,7 +47,11 @@ class Graphics void loadFonts(); void initializeImageLists(); + std::map> cachedAnimations; + public: + std::shared_ptr getAnimation(const std::string & path); + //Fonts static const int FONTS_NUMBER = 9; std::array< std::shared_ptr, FONTS_NUMBER> fonts; diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 08bed6bbc..b272d7d9f 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -422,10 +422,10 @@ CGarrisonSlot::CGarrisonSlot(CGarrisonInt * Owner, int x, int y, SlotID IID, EGa std::string imgName = owner->smallIcons ? "cprsmall" : "TWCRPORT"; - creatureImage = std::make_shared(imgName, 0); + creatureImage = std::make_shared(graphics->getAnimation(imgName), 0); creatureImage->disable(); - selectionImage = std::make_shared(imgName, 1); + selectionImage = std::make_shared(graphics->getAnimation(imgName), 1); selectionImage->disable(); if(Owner->smallIcons) From 4fdc022cdf6f95da81540f879addd69f83ccb1da Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 3 Aug 2023 23:48:55 +0300 Subject: [PATCH 115/168] Version bump to 1.3.1 --- ChangeLog.md | 3 +++ android/vcmi-app/build.gradle | 4 ++-- cmake_modules/VersionDefinition.cmake | 2 +- debian/changelog | 6 ++++++ launcher/eu.vcmi.VCMI.metainfo.xml | 1 + 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 24edce517..677e55f26 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,6 @@ +# 1.3.0 -> 1.3.1 +(unreleased) + # 1.2.1 -> 1.3.0 ### GENERAL: diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 1d818d14a..4f4fc1133 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 31 - versionCode 1302 - versionName "1.3.0" + versionCode 1310 + versionName "1.3.1" setProperty("archivesBaseName", "vcmi") } diff --git a/cmake_modules/VersionDefinition.cmake b/cmake_modules/VersionDefinition.cmake index 5b8409584..e90590d32 100644 --- a/cmake_modules/VersionDefinition.cmake +++ b/cmake_modules/VersionDefinition.cmake @@ -1,6 +1,6 @@ set(VCMI_VERSION_MAJOR 1) set(VCMI_VERSION_MINOR 3) -set(VCMI_VERSION_PATCH 0) +set(VCMI_VERSION_PATCH 1) add_definitions( -DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR} -DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR} diff --git a/debian/changelog b/debian/changelog index 25cc2af65..f7e3380cc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vcmi (1.3.1) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 18 Aug 2023 16:00:00 +0200 + vcmi (1.3.0) jammy; urgency=medium * New upstream release diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index aac246f10..f89354b39 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -51,6 +51,7 @@ StrategyGame + From d459b3b52dc6488be79f4fddbfa102c9a94ddc38 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 5 Aug 2023 23:49:15 +0300 Subject: [PATCH 116/168] Updated changelog --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 677e55f26..3ba074a01 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,11 @@ # 1.3.0 -> 1.3.1 (unreleased) +* Fixed crash on starting game with outdated mods +* Fixed Android mod manager crash +* Fixed framerate drops on hero movement with active hota mod +* Reverted FPS limit on mobile systems back to 60 fps + # 1.2.1 -> 1.3.0 ### GENERAL: From a4297ebdf618510d089b34822278322464ae33f2 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 6 Aug 2023 08:57:14 +0300 Subject: [PATCH 117/168] NKAI: fix potential concurrency and town treat calculation --- .../Analyzers/DangerHitMapAnalyzer.cpp | 62 +++++++++++-------- .../Analyzers/DangerHitMapAnalyzer.h | 15 ++++- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index efc4bde8e..3651e567f 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -53,6 +53,13 @@ void DangerHitMapAnalyzer::updateHitMap() } } + auto ourTowns = cb->getTownsInfo(); + + for(auto town : ourTowns) + { + townTreats[town->id]; // insert empty list + } + foreach_tile_pos([&](const int3 & pos){ hitMap[pos.x][pos.y][pos.z].reset(); }); @@ -95,33 +102,33 @@ void DangerHitMapAnalyzer::updateHitMap() node.fastestDanger = newTreat; } - if(newTreat.turn == 0) + auto objects = cb->getVisitableObjs(pos, false); + + for(auto obj : objects) { - auto objects = cb->getVisitableObjs(pos, false); - - for(auto obj : objects) + if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) { - if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) - enemyHeroAccessibleObjects[path.targetHero].insert(obj); + auto & treats = townTreats[obj->id]; + auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool + { + return i.hero.hid == path.targetHero->id; + }); - if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) + if(treat == treats.end()) { - auto & treats = townTreats[obj->id]; - auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool - { - return i.hero.hid == path.targetHero->id; - }); + treats.emplace_back(); + treat = std::prev(treats.end(), 1); + } - if(treat == treats.end()) - { - treats.emplace_back(); - treat = std::prev(treats.end(), 1); - } + if(newTreat.value() > treat->value()) + { + *treat = newTreat; + } - if(newTreat.value() > treat->value()) - { - *treat = newTreat; - } + if(newTreat.turn == 0) + { + if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) + enemyHeroAccessibleObjects.emplace_back(path.targetHero, obj); } } } @@ -274,16 +281,17 @@ const HitMapNode & DangerHitMapAnalyzer::getTileTreat(const int3 & tile) const const std::set empty = {}; -const std::set & DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const +std::set DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const { - auto result = enemyHeroAccessibleObjects.find(enemy); - - if(result == enemyHeroAccessibleObjects.end()) + std::set result; + + for(auto & obj : enemyHeroAccessibleObjects) { - return empty; + if(obj.hero == enemy) + result.insert(obj.obj); } - return result->second; + return result; } void DangerHitMapAnalyzer::reset() diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 79c56c2c4..614312649 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -55,11 +55,22 @@ struct HitMapNode } }; +struct EnemyHeroAccessibleObject +{ + const CGHeroInstance * hero; + const CGObjectInstance * obj; + + EnemyHeroAccessibleObject(const CGHeroInstance * hero, const CGObjectInstance * obj) + :hero(hero), obj(obj) + { + } +}; + class DangerHitMapAnalyzer { private: boost::multi_array hitMap; - std::map> enemyHeroAccessibleObjects; + tbb::concurrent_vector enemyHeroAccessibleObjects; bool hitMapUpToDate = false; bool tileOwnersUpToDate = false; const Nullkiller * ai; @@ -73,7 +84,7 @@ public: uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; const HitMapNode & getTileTreat(const int3 & tile) const; - const std::set & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; + std::set getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); void resetTileOwners() { tileOwnersUpToDate = false; } PlayerColor getTileOwner(const int3 & tile) const; From 60f0a4553addd11445d4f840e8132e19fa5a9471 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 5 Aug 2023 13:49:49 +0300 Subject: [PATCH 118/168] NKAI: fix freeze on army gathering --- .../Behaviors/GatherArmyBehavior.cpp | 28 +++++++++---------- AI/Nullkiller/Engine/Nullkiller.cpp | 5 ++++ AI/VCAI/VCAI.cpp | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index f5eb28c79..c73b374c0 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -92,15 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her continue; } - bool garrisoned = false; - - if(path.turn() == 0 && hero->inTownGarrison) - { -#if NKAI_TRACE_LEVEL >= 1 - garrisoned = true; -#endif - } - if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) { #if NKAI_TRACE_LEVEL >= 2 @@ -184,15 +175,22 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her composition.addNext(heroExchange); - if(garrisoned && path.turn() == 0) + if(hero->inTownGarrison && path.turn() == 0) { auto lockReason = ai->nullkiller->getHeroLockedReason(hero); - composition.addNextSequence({ - sptr(ExchangeSwapTownHeroes(hero->visitedTown)), - sptr(exchangePath), - sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason)) - }); + if(path.targetHero->visitedTown == hero->visitedTown) + { + composition.addNextSequence({ + sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))}); + } + else + { + composition.addNextSequence({ + sptr(ExchangeSwapTownHeroes(hero->visitedTown)), + sptr(exchangePath), + sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))}); + } } else { diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 66b28ca8e..d6d7f41dc 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -323,6 +323,11 @@ void Nullkiller::makeTurn() } executeTask(bestTask); + + if(i == MAXPASS) + { + logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", bestTask->toString()); + } } } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index c3bd49b6d..f20ceda1f 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1359,7 +1359,7 @@ void VCAI::wander(HeroPtr h) TimeCheck tc("looking for wander destination"); - while(h->movementPointsRemaining()) + for(int k = 0; k < 10 && h->movementPointsRemaining(); k++) { validateVisitableObjs(); ah->updatePaths(getMyHeroes()); From a81f18765c119a4c11c24e121f187d61dedc6324 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 12:45:20 +0300 Subject: [PATCH 119/168] Another workaround for another crash on Android launcher --- .../src/main/java/eu/vcmi/vcmi/util/FileUtil.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java index e64dd3afa..49b731425 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java @@ -81,7 +81,7 @@ public class FileUtil { if (file == null) { - Log.e("Broken path given to fileutil"); + Log.e("Broken path given to fileutil::ensureWriteable"); return false; } @@ -99,6 +99,12 @@ public class FileUtil public static boolean clearDirectory(final File dir) { + if (dir == null) + { + Log.e("Broken path given to fileutil::clearDirectory"); + return false; + } + for (final File f : dir.listFiles()) { if (f.isDirectory() && !clearDirectory(f)) From 0dca5c45bd90f3b6e48a0abbe49f46dd8cc1bc42 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 12:46:29 +0300 Subject: [PATCH 120/168] Workaround for potential crash on opening town dwelling with no valid creatures --- client/windows/CCastleInterface.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index dda745cea..c5a1327bf 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -849,7 +849,13 @@ void CCastleBuildings::enterCastleGate() void CCastleBuildings::enterDwelling(int level) { - assert(level >= 0 && level < town->creatures.size()); + if (level < 0 || level >= town->creatures.size() || town->creatures[level].second.empty()) + { + assert(0); + logGlobal->error("Attempt to enter into invalid dwelling of level %d in town %s (%s)", level, town->getNameTranslated(), town->town->faction->getNameTranslated()); + return; + } + auto recruitCb = [=](CreatureID id, int count) { LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); From a4ff408c9decc0038cfa1206b35c219b62edd0fb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 13:29:57 +0300 Subject: [PATCH 121/168] Removed unused code --- client/render/Graphics.cpp | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index bb9e385e9..6212b357b 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -126,24 +126,11 @@ void Graphics::initializeBattleGraphics() } Graphics::Graphics() { - #if 0 - - std::vector tasks; //preparing list of graphics to load - tasks += std::bind(&Graphics::loadFonts,this); - tasks += std::bind(&Graphics::loadPaletteAndColors,this); - tasks += std::bind(&Graphics::initializeBattleGraphics,this); - tasks += std::bind(&Graphics::loadErmuToPicture,this); - tasks += std::bind(&Graphics::initializeImageLists,this); - - CThreadHelper th(&tasks,std::max((ui32)1,boost::thread::hardware_concurrency())); - th.run(); - #else loadFonts(); loadPaletteAndColors(); initializeBattleGraphics(); loadErmuToPicture(); initializeImageLists(); - #endif //(!) do not load any CAnimation here } From 8b0f9c86f886f6a0c96b923c320f6ba164f12ce2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 13:30:20 +0300 Subject: [PATCH 122/168] Fixed possible crash on invalid town building name --- lib/CTownHandler.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index f756c79de..ee47103db 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -1183,11 +1183,19 @@ void CTownHandler::initializeRequirements() { if (node.Vector().size() > 1) { - logMod->warn("Unexpected length of town buildings requirements: %d", node.Vector().size()); - logMod->warn("Entry contains: "); - logMod->warn(node.toJson()); + logMod->error("Unexpected length of town buildings requirements: %d", node.Vector().size()); + logMod->error("Entry contains: "); + logMod->error(node.toJson()); } - return BuildingID(VLC->modh->identifiers.getIdentifier(requirement.town->getBuildingScope(), node.Vector()[0]).value()); + + auto index = VLC->modh->identifiers.getIdentifier(requirement.town->getBuildingScope(), node[0]); + + if (!index.has_value()) + { + logMod->error("Unknown building in town buildings: %s", node[0].String()); + return BuildingID::NONE; + } + return BuildingID(index.value()); }); } requirementsToLoad.clear(); From 5faaf175f9cc97042b18745623ef54e7a9fd84e6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 13:31:10 +0300 Subject: [PATCH 123/168] Added debug information to SDL-related crashes --- client/CMT.cpp | 11 +++++++++++ client/CMT.h | 4 ++++ client/renderSDL/SDL_Extensions.cpp | 11 +++++++++++ client/renderSDL/ScreenHandler.cpp | 10 +++++++++- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 3f4fe83b4..4e050afab 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -38,6 +38,7 @@ #include #include +#include #ifdef VCMI_ANDROID #include "../lib/CAndroidVMHelper.h" @@ -510,3 +511,13 @@ void handleQuit(bool ask) quitApplication(); } } + +void handleFatalError(const std::string & message) +{ + logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE"); + logGlobal->error("Reason: %s", message); + + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error", message.c_str(), nullptr); + + throw std::runtime_error(message); +} diff --git a/client/CMT.h b/client/CMT.h index 887b704c9..cfea318ff 100644 --- a/client/CMT.h +++ b/client/CMT.h @@ -21,3 +21,7 @@ extern SDL_Surface *screen2; // and hlp surface (used to store not-active in extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed void handleQuit(bool ask = true); + +/// Notify user about encoutered fatal error and terminate the game +/// TODO: decide on better location for this method +[[noreturn]] void handleFatalError(const std::string & message); diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index b362e137f..3d6323cd5 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -80,6 +80,17 @@ SDL_Surface * CSDL_Ext::newSurface(int w, int h) SDL_Surface * CSDL_Ext::newSurface(int w, int h, SDL_Surface * mod) //creates new surface, with flags/format same as in surface given { SDL_Surface * ret = SDL_CreateRGBSurface(0,w,h,mod->format->BitsPerPixel,mod->format->Rmask,mod->format->Gmask,mod->format->Bmask,mod->format->Amask); + + if(ret == nullptr) + { + const char * error = SDL_GetError(); + + std::string messagePattern = "Failed to create SDL Surface of size %d x %d, %d bpp. Reason: %s"; + std::string message = boost::str(boost::format(messagePattern) % w % h % mod->format->BitsPerPixel % error); + + handleFatalError(message); + } + if (mod->format->palette) { assert(ret->format->palette); diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index 8de235d47..8f071cbe1 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -264,7 +264,15 @@ void ScreenHandler::initializeWindow() mainWindow = createWindow(); if(mainWindow == nullptr) - throw std::runtime_error("Unable to create window\n"); + { + const char * error = SDL_GetError(); + Point dimensions = getPreferredWindowResolution(); + + std::string messagePattern = "Failed to create SDL Window of size %d x %d. Reason: %s"; + std::string message = boost::str(boost::format(messagePattern) % dimensions.x % dimensions.y % error); + + handleFatalError(message); + } //create first available renderer if preferred not set. Use no flags, so HW accelerated will be preferred but SW renderer also will possible mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), 0); From 702f57d5cc93d4e07dff51ad976c7c9fba654ee4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 13:43:08 +0300 Subject: [PATCH 124/168] Avoid another crash in Android launcher --- .../java/eu/vcmi/vcmi/settings/ExportDataController.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/ExportDataController.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/ExportDataController.java index b799d10c1..35fdb6e2d 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/ExportDataController.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/ExportDataController.java @@ -133,6 +133,12 @@ public class ExportDataController extends LauncherSettingController } } + if (exported == null) + { + publishProgress("Failed to copy file " + child.getName()); + return false; + } + try( final OutputStream targetStream = owner.getContentResolver() .openOutputStream(exported.getUri()); From 48f130cd763b7ff4b3034d570545ee063abb30a5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 14:11:14 +0300 Subject: [PATCH 125/168] Fix crash on haptic feedback on Android --- android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index 8470e2d16..55ca15691 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -146,7 +146,7 @@ public class NativeMethods public static void hapticFeedback() { final Context ctx = SDL.getContext(); - if (Build.VERSION.SDK_INT >= 26) { + if (Build.VERSION.SDK_INT >= 29) { ((Vibrator) ctx.getSystemService(ctx.VIBRATOR_SERVICE)).vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)); } else { ((Vibrator) ctx.getSystemService(ctx.VIBRATOR_SERVICE)).vibrate(30); From 0c9e2d6fddfa8aebb2205074f8ae203f86892baa Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 14:11:31 +0300 Subject: [PATCH 126/168] Workaround for crash on map rendering --- client/mapView/MapRenderer.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index a9f8b63b6..0b073177a 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -523,6 +523,14 @@ uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 & for(const auto & objectID : context.getObjects(coordinates)) { const auto * objectInstance = context.getObject(objectID); + + assert(objectInstance); + if(!objectInstance) + { + logGlobal->error("Stray map object that isn't fading"); + continue; + } + size_t groupIndex = context.objectGroupIndex(objectInstance->id); Point offsetPixels = context.objectImageOffset(objectInstance->id, coordinates); From 9e4a42b25df2bf2abbe6c587c3434ac1dfe15d98 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 14:33:20 +0300 Subject: [PATCH 127/168] Android build ID bump --- android/vcmi-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 1d818d14a..226f435f3 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,7 +10,7 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 31 - versionCode 1302 + versionCode 1303 versionName "1.3.0" setProperty("archivesBaseName", "vcmi") } From c524caee5c9055fda30c95a6b030402a10d4f290 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 17:46:49 +0300 Subject: [PATCH 128/168] Fixed possible uncaught exception on starting map --- client/CServerHandler.cpp | 13 +++++++++++-- client/CServerHandler.h | 2 +- lib/StartInfo.cpp | 6 +++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 225c09c9e..18e9ee444 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -572,7 +572,16 @@ void CServerHandler::sendRestartGame() const void CServerHandler::sendStartGame(bool allowOnlyAI) const { - verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); + try + { + verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); + } + catch (const std::exception & e) + { + showServerError( std::string("Unable to start map! Reason: ") + e.what()); + return; + } + LobbyStartGame lsg; if(client) { @@ -696,7 +705,7 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) }); } -void CServerHandler::showServerError(std::string txt) +void CServerHandler::showServerError(std::string txt) const { CInfoWindow::showInfoDialog(txt, {}); } diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 199aa04a8..4b3e61c65 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -151,7 +151,7 @@ public: void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); void endGameplay(bool closeConnection = true, bool restart = false); void startCampaignScenario(std::shared_ptr cs = {}); - void showServerError(std::string txt); + void showServerError(std::string txt) const; // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle int howManyPlayerInterfaces(); diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index c722c80d5..6338a1541 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -71,7 +71,7 @@ std::string StartInfo::getCampaignName() const void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const { if(!mi || !mi->mapHeader) - throw std::domain_error("ExceptionMapMissing"); + throw std::domain_error("There is no map to start!"); auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader); CModHandler::Incompatibility::ModList modList; @@ -88,12 +88,12 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const break; if(i == si->playerInfos.cend() && !ignoreNoHuman) - throw std::domain_error("ExceptionNoHuman"); + throw std::domain_error("There is no human player on map"); if(si->mapGenOptions && si->mode == StartInfo::NEW_GAME) { if(!si->mapGenOptions->checkOptions()) - throw std::domain_error("ExceptionNoTemplate"); + throw std::domain_error("No random map template found!"); } } From 6ddac8376d9c06ee580f0c95dc3bc24ee988ef5e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 17:47:12 +0300 Subject: [PATCH 129/168] Fixed possible crash on attempt to load missing font(?) --- client/renderSDL/CBitmapFont.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/renderSDL/CBitmapFont.cpp b/client/renderSDL/CBitmapFont.cpp index 0810b649b..fd4be26c1 100644 --- a/client/renderSDL/CBitmapFont.cpp +++ b/client/renderSDL/CBitmapFont.cpp @@ -26,6 +26,12 @@ void CBitmapFont::loadModFont(const std::string & modName, const ResourceID & resource) { + if (!CResourceHandler::get(modName)->existsResource(resource)) + { + logGlobal->error("Failed to load font %s from mod %s", resource.getName(), modName); + return; + } + auto data = CResourceHandler::get(modName)->load(resource)->readAll(); std::string modLanguage = CGI->modh->getModLanguage(modName); std::string modEncoding = Languages::getLanguageOptions(modLanguage).encoding; From 0b99fc0ccb992c6a2e162242e1ee66505220d1e6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 18:12:36 +0300 Subject: [PATCH 130/168] Show message box when H3 data is missing --- client/CMT.cpp | 26 ++++++++++++-------------- client/CMT.h | 2 +- client/renderSDL/SDL_Extensions.cpp | 2 +- client/renderSDL/ScreenHandler.cpp | 2 +- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 4e050afab..b744aee4c 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -261,19 +261,12 @@ int main(int argc, char * argv[]) if (CResourceHandler::get()->existsResource(ResourceID(filename))) return true; - logGlobal->error("Error: %s was not found!", message); - return false; + handleFatalError(message, false); }; - if (!testFile("DATA/HELP.TXT", "Heroes III data") || - !testFile("MODS/VCMI/MOD.JSON", "VCMI data")) - { - exit(1); // These are unrecoverable errors - } - - // these two are optional + some installs have them on CD and not in data directory - testFile("VIDEO/GOOD1A.SMK", "campaign movies"); - testFile("SOUNDS/G1A.WAV", "campaign music"); //technically not a music but voiced intro sounds + 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/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!"); srand ( (unsigned int)time(nullptr) ); @@ -512,12 +505,17 @@ void handleQuit(bool ask) } } -void handleFatalError(const std::string & message) +void handleFatalError(const std::string & message, bool terminate) { logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE"); logGlobal->error("Reason: %s", message); - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error", message.c_str(), nullptr); + std::string messageToShow = "Fatal error! " + message; - throw std::runtime_error(message); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error!", messageToShow.c_str(), nullptr); + + if (terminate) + throw std::runtime_error(message); + else + exit(1); } diff --git a/client/CMT.h b/client/CMT.h index cfea318ff..28d877875 100644 --- a/client/CMT.h +++ b/client/CMT.h @@ -24,4 +24,4 @@ void handleQuit(bool ask = true); /// Notify user about encoutered fatal error and terminate the game /// TODO: decide on better location for this method -[[noreturn]] void handleFatalError(const std::string & message); +[[noreturn]] void handleFatalError(const std::string & message, bool terminate); diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 3d6323cd5..b4d7f1249 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -88,7 +88,7 @@ SDL_Surface * CSDL_Ext::newSurface(int w, int h, SDL_Surface * mod) //creates ne std::string messagePattern = "Failed to create SDL Surface of size %d x %d, %d bpp. Reason: %s"; std::string message = boost::str(boost::format(messagePattern) % w % h % mod->format->BitsPerPixel % error); - handleFatalError(message); + handleFatalError(message, true); } if (mod->format->palette) diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index 8f071cbe1..623289074 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -271,7 +271,7 @@ void ScreenHandler::initializeWindow() std::string messagePattern = "Failed to create SDL Window of size %d x %d. Reason: %s"; std::string message = boost::str(boost::format(messagePattern) % dimensions.x % dimensions.y % error); - handleFatalError(message); + handleFatalError(message, true); } //create first available renderer if preferred not set. Use no flags, so HW accelerated will be preferred but SW renderer also will possible From 9bd27c50a0829f595b71b49a0d8515895ff5c673 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Aug 2023 19:39:55 +0300 Subject: [PATCH 131/168] Allowed loading saves from inside mods --- client/Client.cpp | 2 +- client/lobby/SelectionTab.cpp | 2 +- server/CGameHandler.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/Client.cpp b/client/Client.cpp index 35e7c1649..eec33cb18 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -219,7 +219,7 @@ void CClient::loadGame(CGameState * initializedGameState) // try to deserialize client data including sleepingHeroes try { - boost::filesystem::path clientSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); + boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourceID(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); if(clientSaveName.empty()) throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 84b430f32..09c4c6149 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -426,7 +426,7 @@ void SelectionTab::select(int position) if(inputName && inputName->isActive()) { - auto filename = *CResourceHandler::get("local")->getResourceName(ResourceID(curItems[py]->fileURI, EResType::SAVEGAME)); + auto filename = *CResourceHandler::get()->getResourceName(ResourceID(curItems[py]->fileURI, EResType::SAVEGAME)); inputName->setText(filename.stem().string()); } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 5b6385ac0..6308e1083 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2942,7 +2942,7 @@ bool CGameHandler::load(const std::string & filename) try { { - CLoadFile lf(*CResourceHandler::get("local")->getResourceName(ResourceID(stem.to_string(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourceID(stem.to_string(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); loadCommonState(lf); logGlobal->info("Loading server state"); lf >> *this; From c089e558939734fa7b3c098ee785cae1e31e9aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 6 Aug 2023 21:14:16 +0200 Subject: [PATCH 132/168] Fix placing road inside underground rock --- lib/rmg/RmgArea.cpp | 6 ++++++ lib/rmg/RmgArea.h | 1 + lib/rmg/modificators/RoadPlacer.cpp | 10 ++++++++++ 3 files changed, 17 insertions(+) diff --git a/lib/rmg/RmgArea.cpp b/lib/rmg/RmgArea.cpp index 3131bf188..1b6b6196e 100644 --- a/lib/rmg/RmgArea.cpp +++ b/lib/rmg/RmgArea.cpp @@ -376,6 +376,12 @@ void Area::translate(const int3 & shift) //toAbsolute(dTiles, shift); } +void Area::erase_if(std::function predicate) +{ + invalidate(); + vstd::erase_if(dTiles, predicate); +} + Area operator- (const Area & l, const int3 & r) { Area result(l); diff --git a/lib/rmg/RmgArea.h b/lib/rmg/RmgArea.h index 1755f22d8..f07bcd5e0 100644 --- a/lib/rmg/RmgArea.h +++ b/lib/rmg/RmgArea.h @@ -64,6 +64,7 @@ namespace rmg void intersect(const Area & area); void subtract(const Area & area); void translate(const int3 & shift); + void erase_if(std::function predicate); friend Area operator+ (const Area & l, const int3 & r); //translation friend Area operator- (const Area & l, const int3 & r); //translation diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index 498c4fab0..c7e30e0a2 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -18,9 +18,12 @@ #include "../threadpool/MapProxy.h" #include "../../CModHandler.h" #include "../../mapping/CMapEditManager.h" +#include "../../TerrainHandler.h" VCMI_LIB_NAMESPACE_BEGIN +class TerrainType; + void RoadPlacer::process() { if(generator.getConfig().defaultRoadType.empty() && generator.getConfig().secondaryRoadType.empty()) @@ -114,6 +117,13 @@ void RoadPlacer::drawRoads(bool secondary) //Clean space under roads even if they won't be eventually generated Zone::Lock lock(zone.areaMutex); + //Do not draw roads on underground rock or water + roads.erase_if([this](const int3& pos) -> bool + { + const auto* terrain = map.getTile(pos).terType;; + return !terrain->isPassable() || !terrain->isLand(); + }); + zone.areaPossible().subtract(roads); zone.freePaths().unite(roads); } From b5fc0f19f23a7725602723b4c302d4b9f095a309 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 7 Aug 2023 16:53:51 +0300 Subject: [PATCH 133/168] Proper fix for Android launcher nullptr dereference --- android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java index 49b731425..887c8ba9c 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java @@ -99,7 +99,7 @@ public class FileUtil public static boolean clearDirectory(final File dir) { - if (dir == null) + if (dir == null || dir.listFiles() == null) { Log.e("Broken path given to fileutil::clearDirectory"); return false; From b167aeafd850b42d667a0728b017194da8032f3b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 7 Aug 2023 17:03:19 +0300 Subject: [PATCH 134/168] Build ID bump for Android --- android/vcmi-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 226f435f3..0a00e9de0 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,7 +10,7 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 31 - versionCode 1303 + versionCode 1304 versionName "1.3.0" setProperty("archivesBaseName", "vcmi") } From d7cbe4ecde8ae8fb56350886fb00db70ff8c17b2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 7 Aug 2023 17:03:39 +0300 Subject: [PATCH 135/168] Attempt to fix data raced in battle on server --- server/CGameHandler.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 5b6385ac0..3cb0f5655 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -40,6 +40,7 @@ #include "../lib/CCreatureHandler.h" #include "../lib/gameState/CGameState.h" #include "../lib/CStack.h" +#include "../lib/UnlockGuard.h" #include "../lib/GameSettings.h" #include "../lib/battle/BattleInfo.h" #include "../lib/CondSh.h" @@ -76,6 +77,7 @@ #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; @@ -4394,6 +4396,8 @@ void CGameHandler::updateGateState() bool CGameHandler::makeBattleAction(BattleAction &ba) { + boost::unique_lock lock(battleActionMutex); + bool ok = true; battle::Target target = ba.getTarget(gs->curB); @@ -4817,6 +4821,8 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) bool CGameHandler::makeCustomAction(BattleAction & ba) { + boost::unique_lock lock(battleActionMutex); + switch(ba.actionType) { case EActionType::HERO_SPELL: @@ -6048,6 +6054,8 @@ bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & s void CGameHandler::runBattle() { + boost::unique_lock lock(battleActionMutex); + setBattle(gs->curB); assert(gs->curB); //TODO: pre-tactic stuff, call scripts etc. @@ -6066,7 +6074,10 @@ void CGameHandler::runBattle() //tactic round { while ((lobby->state != EServerState::SHUTDOWN) && gs->curB->tacticDistance && !battleResult.get()) + { + auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } } //initial stacks appearance triggers, e.g. built-in bonus spells @@ -6389,7 +6400,10 @@ void CGameHandler::runBattle() battleMadeAction.data = false; while ((lobby->state != EServerState::SHUTDOWN) && !actionWasMade()) { - battleMadeAction.cond.wait(lock); + { + auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); + battleMadeAction.cond.wait(lock); + } if (battleGetStackByID(nextId, false) != next) next = nullptr; //it may be removed, while we wait } From 04fe78d31cd915d58a7b85dcaa6ee62a57e32111 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 5 Aug 2023 13:49:49 +0300 Subject: [PATCH 136/168] NKAI: fix freeze on army gathering --- .../Behaviors/GatherArmyBehavior.cpp | 28 +++++++++---------- AI/Nullkiller/Engine/Nullkiller.cpp | 5 ++++ AI/VCAI/VCAI.cpp | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index f5eb28c79..c73b374c0 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -92,15 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her continue; } - bool garrisoned = false; - - if(path.turn() == 0 && hero->inTownGarrison) - { -#if NKAI_TRACE_LEVEL >= 1 - garrisoned = true; -#endif - } - if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path)) { #if NKAI_TRACE_LEVEL >= 2 @@ -184,15 +175,22 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her composition.addNext(heroExchange); - if(garrisoned && path.turn() == 0) + if(hero->inTownGarrison && path.turn() == 0) { auto lockReason = ai->nullkiller->getHeroLockedReason(hero); - composition.addNextSequence({ - sptr(ExchangeSwapTownHeroes(hero->visitedTown)), - sptr(exchangePath), - sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason)) - }); + if(path.targetHero->visitedTown == hero->visitedTown) + { + composition.addNextSequence({ + sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))}); + } + else + { + composition.addNextSequence({ + sptr(ExchangeSwapTownHeroes(hero->visitedTown)), + sptr(exchangePath), + sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))}); + } } else { diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 66b28ca8e..d6d7f41dc 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -323,6 +323,11 @@ void Nullkiller::makeTurn() } executeTask(bestTask); + + if(i == MAXPASS) + { + logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", bestTask->toString()); + } } } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index c3bd49b6d..f20ceda1f 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1359,7 +1359,7 @@ void VCAI::wander(HeroPtr h) TimeCheck tc("looking for wander destination"); - while(h->movementPointsRemaining()) + for(int k = 0; k < 10 && h->movementPointsRemaining(); k++) { validateVisitableObjs(); ah->updatePaths(getMyHeroes()); From 1eb58bcc32d6cb98b0c9e21ad0d5adc2685c0388 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 6 Aug 2023 08:57:14 +0300 Subject: [PATCH 137/168] NKAI: fix potential concurrency and town treat calculation --- .../Analyzers/DangerHitMapAnalyzer.cpp | 62 +++++++++++-------- .../Analyzers/DangerHitMapAnalyzer.h | 15 ++++- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index efc4bde8e..3651e567f 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -53,6 +53,13 @@ void DangerHitMapAnalyzer::updateHitMap() } } + auto ourTowns = cb->getTownsInfo(); + + for(auto town : ourTowns) + { + townTreats[town->id]; // insert empty list + } + foreach_tile_pos([&](const int3 & pos){ hitMap[pos.x][pos.y][pos.z].reset(); }); @@ -95,33 +102,33 @@ void DangerHitMapAnalyzer::updateHitMap() node.fastestDanger = newTreat; } - if(newTreat.turn == 0) + auto objects = cb->getVisitableObjs(pos, false); + + for(auto obj : objects) { - auto objects = cb->getVisitableObjs(pos, false); - - for(auto obj : objects) + if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) { - if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) - enemyHeroAccessibleObjects[path.targetHero].insert(obj); + auto & treats = townTreats[obj->id]; + auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool + { + return i.hero.hid == path.targetHero->id; + }); - if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) + if(treat == treats.end()) { - auto & treats = townTreats[obj->id]; - auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool - { - return i.hero.hid == path.targetHero->id; - }); + treats.emplace_back(); + treat = std::prev(treats.end(), 1); + } - if(treat == treats.end()) - { - treats.emplace_back(); - treat = std::prev(treats.end(), 1); - } + if(newTreat.value() > treat->value()) + { + *treat = newTreat; + } - if(newTreat.value() > treat->value()) - { - *treat = newTreat; - } + if(newTreat.turn == 0) + { + if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) + enemyHeroAccessibleObjects.emplace_back(path.targetHero, obj); } } } @@ -274,16 +281,17 @@ const HitMapNode & DangerHitMapAnalyzer::getTileTreat(const int3 & tile) const const std::set empty = {}; -const std::set & DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const +std::set DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const { - auto result = enemyHeroAccessibleObjects.find(enemy); - - if(result == enemyHeroAccessibleObjects.end()) + std::set result; + + for(auto & obj : enemyHeroAccessibleObjects) { - return empty; + if(obj.hero == enemy) + result.insert(obj.obj); } - return result->second; + return result; } void DangerHitMapAnalyzer::reset() diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 79c56c2c4..614312649 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -55,11 +55,22 @@ struct HitMapNode } }; +struct EnemyHeroAccessibleObject +{ + const CGHeroInstance * hero; + const CGObjectInstance * obj; + + EnemyHeroAccessibleObject(const CGHeroInstance * hero, const CGObjectInstance * obj) + :hero(hero), obj(obj) + { + } +}; + class DangerHitMapAnalyzer { private: boost::multi_array hitMap; - std::map> enemyHeroAccessibleObjects; + tbb::concurrent_vector enemyHeroAccessibleObjects; bool hitMapUpToDate = false; bool tileOwnersUpToDate = false; const Nullkiller * ai; @@ -73,7 +84,7 @@ public: uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; const HitMapNode & getTileTreat(const int3 & tile) const; - const std::set & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; + std::set getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); void resetTileOwners() { tileOwnersUpToDate = false; } PlayerColor getTileOwner(const int3 & tile) const; From f0ede46186514f67a0ab23c1fb652b09e50c95f2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 7 Aug 2023 19:12:04 +0300 Subject: [PATCH 138/168] Clear spell list, not hero army --- lib/mapping/MapFormatH3M.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 1df7ff008..e7ab538a9 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1736,7 +1736,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec { if(!object->spells.empty()) { - object->clear(); + object->spells.clear(); logGlobal->debug("Hero %s subID=%d has spells set twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); } From e57f8742cd888c80184283335751d1d855609a14 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 7 Aug 2023 19:13:02 +0300 Subject: [PATCH 139/168] Rename ambiguos 'clear' to 'clearSlots' A lot of map objects inherit from CCreatureSet and as result - get clean() method that resets object army --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 2 +- lib/CCreatureSet.cpp | 8 ++++---- lib/CCreatureSet.h | 6 +++--- lib/NetPacks.h | 2 +- lib/mapObjects/CBank.cpp | 2 +- mapeditor/inspector/rewardswidget.cpp | 2 +- server/HeroPoolProcessor.cpp | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 37dfb7a76..19f48ed65 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -170,7 +170,7 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, std::vector newArmy; uint64_t newValue = 0; - newArmyInstance.clear(); + newArmyInstance.clearSlots(); for(auto & slot : sortedSlots) { diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 084fb1e40..a11ede6ab 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -445,7 +445,7 @@ void CCreatureSet::setStackExp(const SlotID & slot, TExpType exp) stacks[slot]->experience = exp; } -void CCreatureSet::clear() +void CCreatureSet::clearSlots() { while(!stacks.empty()) { @@ -533,12 +533,12 @@ void CCreatureSet::changeStackCount(const SlotID & slot, TQuantity toAdd) CCreatureSet::~CCreatureSet() { - clear(); + clearSlots(); } void CCreatureSet::setToArmy(CSimpleArmy &src) { - clear(); + clearSlots(); while(src) { auto i = src.army.begin(); @@ -1050,7 +1050,7 @@ void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) } } -void CSimpleArmy::clear() +void CSimpleArmy::clearSlots() { army.clear(); } diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 09794f3c8..4ef0e7ea7 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -181,7 +181,7 @@ using TCreatureQueue = std::priority_queue(config); - clear(); // remove all stacks, if any + clearSlots(); // remove all stacks, if any for(const auto & stack : config.guards) setCreature (SlotID(stacksCount()), stack.type->getId(), stack.count); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 64dcc9a78..ea2b94e9e 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -222,7 +222,7 @@ bool RewardsWidget::commitChanges() pandora->resources = ResourceSet(); pandora->artifacts.clear(); pandora->spells.clear(); - pandora->creatures.clear(); + pandora->creatures.clearSlots(); for(int row = 0; row < rewards; ++row) { diff --git a/server/HeroPoolProcessor.cpp b/server/HeroPoolProcessor.cpp index 2d50595b0..4c92e5963 100644 --- a/server/HeroPoolProcessor.cpp +++ b/server/HeroPoolProcessor.cpp @@ -112,7 +112,7 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; sah.hid = hero->subID; - sah.army.clear(); + sah.army.clearSlots(); sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1); gameHandler->sendAndApply(&sah); @@ -148,7 +148,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe else { sah.roleID = TavernSlotRole::SINGLE_UNIT; - sah.army.clear(); + sah.army.clearSlots(); sah.army.setCreature(SlotID(0), newHero->type->initialArmy[0].creature, 1); } } From 62a5eeebbcc42e56084b6c2f1ae447092eb285d3 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 7 Aug 2023 21:18:43 +0300 Subject: [PATCH 140/168] Fixed rendering priority of battlefield background obstacles --- client/battle/BattleObstacleController.cpp | 28 +++++++++++++++------- client/battle/BattleRenderer.h | 3 +-- client/battle/BattleSiegeController.cpp | 4 +--- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index 09f46223a..d41b09a66 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -127,13 +127,26 @@ void BattleObstacleController::obstaclePlaced(const std::vectorcb->battleGetAllObstacles()) + for(auto & obstacle : owner.curInt->cb->battleGetAllObstacles()) { - if(oi->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) + if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { - auto img = getObstacleImage(*oi); + auto img = getObstacleImage(*obstacle); if(img) - canvas.draw(img, Point(oi->getInfo().width, oi->getInfo().height)); + canvas.draw(img, Point(obstacle->getInfo().width, obstacle->getInfo().height)); + } + + if (obstacle->obstacleType == CObstacleInstance::USUAL) + { + if (obstacle->getInfo().isForegroundObstacle) + continue; + + auto img = getObstacleImage(*obstacle); + if(img) + { + Point p = getObstaclePosition(img, *obstacle); + canvas.draw(img, p); + } } } } @@ -148,11 +161,10 @@ void BattleObstacleController::collectRenderableObjects(BattleRenderer & rendere if (obstacle->obstacleType == CObstacleInstance::MOAT) continue; - bool isForeground = obstacle->obstacleType == CObstacleInstance::USUAL && obstacle->getInfo().isForegroundObstacle; + if (obstacle->obstacleType == CObstacleInstance::USUAL && !obstacle->getInfo().isForegroundObstacle) + continue; - auto layer = isForeground ? EBattleFieldLayer::OBSTACLES_FG : EBattleFieldLayer::OBSTACLES_BG; - - renderer.insert(layer, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){ + renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){ auto img = getObstacleImage(*obstacle); if(img) { diff --git a/client/battle/BattleRenderer.h b/client/battle/BattleRenderer.h index fa5d4e15f..0dbab110b 100644 --- a/client/battle/BattleRenderer.h +++ b/client/battle/BattleRenderer.h @@ -16,12 +16,11 @@ class BattleInterface; enum class EBattleFieldLayer { // confirmed ordering requirements: - OBSTACLES_BG = 0, CORPSES = 0, WALLS = 1, HEROES = 2, STACKS = 2, // after corpses, obstacles, walls - OBSTACLES_FG = 3, // after stacks + OBSTACLES = 3, // after stacks STACK_AMOUNTS = 3, // after stacks, obstacles, corpses EFFECTS = 4, // after obstacles, battlements }; diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 46852849c..63d5bd9e5 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -307,15 +307,13 @@ void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer) renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ owner.stacksController->showStack(canvas, getTurretStack(wallPiece)); }); - renderer.insert( EBattleFieldLayer::OBSTACLES_FG, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ + renderer.insert( EBattleFieldLayer::OBSTACLES, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ showWallPiece(canvas, wallPiece); }); } renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ showWallPiece(canvas, wallPiece); }); - - } } From ed066cb7ba2162a872a5c8031d0c698fd06e8fd7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 8 Aug 2023 00:27:03 +0300 Subject: [PATCH 141/168] Fixed possible multithreading races in sound/music callbacks --- client/CMusicHandler.cpp | 89 ++++++++++++++++++++++------------------ client/CMusicHandler.h | 13 ++++-- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index a1a31370b..521d47eb1 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -14,6 +14,7 @@ #include "CMusicHandler.h" #include "CGameInfo.h" #include "renderSDL/SDLRWwrapper.h" +#include "gui/CGuiHandler.h" #include "../lib/JsonNode.h" #include "../lib/GameConstants.h" @@ -185,9 +186,9 @@ int CSoundHandler::playSound(std::string sound, int repeats, bool cache) Mix_FreeChunk(chunk); } else if (cache) - callbacks[channel]; + initCallback(channel); else - callbacks[channel] = [chunk](){ Mix_FreeChunk(chunk);}; + initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); } else channel = -1; @@ -237,28 +238,50 @@ void CSoundHandler::setChannelVolume(int channel, ui32 percent) void CSoundHandler::setCallback(int channel, std::function function) { - std::map >::iterator iter; - iter = callbacks.find(channel); + boost::unique_lock lockGuard(mutexCallbacks); + + auto iter = callbacks.find(channel); //channel not found. It may have finished so fire callback now if(iter == callbacks.end()) function(); else - iter->second = function; + iter->second.push_back(function); } void CSoundHandler::soundFinishedCallback(int channel) { - std::map >::iterator iter; - iter = callbacks.find(channel); - if (iter == callbacks.end()) + boost::unique_lock lockGuard(mutexCallbacks); + + if (callbacks.count(channel) == 0) return; - auto callback = std::move(iter->second); - callbacks.erase(iter); + // store callbacks from container locally - SDL might reuse this channel for another sound + // but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own + auto callback = callbacks.at(channel); + callbacks.erase(channel); - if (callback) - callback(); + if (!callback.empty()) + { + GH.dispatchMainThread([callback](){ + for (auto entry : callback) + entry(); + }); + } +} + +void CSoundHandler::initCallback(int channel) +{ + boost::unique_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel] = {}; +} + +void CSoundHandler::initCallback(int channel, const std::function & function) +{ + boost::unique_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel].push_back(function); } int CSoundHandler::ambientGetRange() const @@ -471,44 +494,32 @@ void CMusicHandler::setVolume(ui32 percent) void CMusicHandler::musicFinishedCallback() { - // boost::mutex::scoped_lock guard(mutex); - // FIXME: WORKAROUND FOR A POTENTIAL DEADLOCK + // call music restart in separate thread to avoid deadlock in some cases // It is possible for: // 1) SDL thread to call this method on end of playback // 2) VCMI code to call queueNext() method to queue new file // this leads to: // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) - // Because of that (and lack of clear way to fix that) - // We will try to acquire lock here and if failed - do nothing - // This may break music playback till next song is enqued but won't deadlock the game - if (!mutex.try_lock()) + GH.dispatchMainThread([this]() { - logGlobal->error("Failed to acquire mutex! Unable to restart music!"); - return; - } - - if (current.get() != nullptr) - { - // if music is looped, play it again - if (current->play()) + boost::unique_lock lockGuard(mutex); + if (current.get() != nullptr) { - mutex.unlock(); - return; + // if music is looped, play it again + if (current->play()) + return; + else + current.reset(); } - else - { - current.reset(); - } - } - if (current.get() == nullptr && next.get() != nullptr) - { - current.reset(next.release()); - current->play(); - } - mutex.unlock(); + if (current.get() == nullptr && next.get() != nullptr) + { + current.reset(next.release()); + current->play(); + } + }); } MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart): diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index 3594cf37d..a0180cd74 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -45,9 +45,13 @@ private: Mix_Chunk *GetSoundChunk(std::string &sound, bool cache); - //have entry for every currently active channel - //std::function will be nullptr if callback was not set - std::map > callbacks; + /// have entry for every currently active channel + /// vector will be empty if callback was not set + std::map> > callbacks; + + /// Protects access to callbacks member to avoid data races: + /// SDL calls sound finished callbacks from audio thread + boost::mutex mutexCallbacks; int ambientDistToVolume(int distance) const; void ambientStopSound(std::string soundId); @@ -58,6 +62,9 @@ private: std::map ambientChannels; std::map channelVolumes; + void initCallback(int channel, const std::function & function); + void initCallback(int channel); + public: CSoundHandler(); From d947c14495cb8dd8caa255f1df43b9879dc6005d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 8 Aug 2023 12:50:16 +0300 Subject: [PATCH 142/168] Few more H3 data validation checks --- client/CMT.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index b744aee4c..7e4feb25f 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -256,16 +256,16 @@ int main(int argc, char * argv[]) logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); // Some basic data validation to produce better error messages in cases of incorrect install - auto testFile = [](std::string filename, std::string message) -> bool + auto testFile = [](std::string filename, std::string message) { - if (CResourceHandler::get()->existsResource(ResourceID(filename))) - return true; - - handleFatalError(message, false); + if (!CResourceHandler::get()->existsResource(ResourceID(filename))) + handleFatalError(message, false); }; 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!"); srand ( (unsigned int)time(nullptr) ); From 2e7e8bb52bc6a1cd59da84dc894d3623f6dcaa58 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 8 Aug 2023 12:50:39 +0300 Subject: [PATCH 143/168] Better error message on missing object constructor --- .../CObjectClassesHandler.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 22265c0cb..99d5aa82c 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -316,11 +316,21 @@ std::vector CObjectClassesHandler::getDefaultAllowed() const TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) const { - assert(type < objects.size()); - assert(objects[type]); - assert(subtype < objects[type]->objects.size()); + try + { + auto result = objects.at(type)->objects.at(subtype); - return objects.at(type)->objects.at(subtype); + if (result != nullptr) + return result; + } + catch (std::out_of_range & e) + { + // Leave catch block silently + } + + std::string errorString = "Failed to find object of type " + std::to_string(type) + "::" + std::to_string(subtype); + logGlobal->error(errorString); + throw std::runtime_error(errorString); } TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const From be6f6da3976bda8e215afeb5c2b9ecf01049b81f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 8 Aug 2023 19:27:00 +0400 Subject: [PATCH 144/168] Fix for canRefuse flag in rewardable building --- lib/mapObjects/CGTownBuilding.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 0cccdc62e..556bc91cb 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -356,6 +356,9 @@ void CTownRewardableBuilding::heroLevelUpDone(const CGHeroInstance *hero) const void CTownRewardableBuilding::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const { + if(answer == 0) + return; // player refused + if(visitors.find(hero->id) != visitors.end()) return; // query not for this building From e7394ad20cd2ce425aedf21186ec7cf726ff033c Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Tue, 8 Aug 2023 18:54:37 +0300 Subject: [PATCH 145/168] BattleAI: log time to make a decission --- AI/BattleAI/BattleAI.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index b3986de94..01947d4d5 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -254,6 +254,13 @@ void CBattleAI::yourTacticPhase(int distance) cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); } +uint64_t timeElapsed(std::chrono::time_point start) +{ + auto end = std::chrono::high_resolution_clock::now(); + + return std::chrono::duration_cast(end - start).count(); +} + void CBattleAI::activeStack( const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); @@ -261,6 +268,8 @@ void CBattleAI::activeStack( const CStack * stack ) BattleAction result = BattleAction::makeDefend(stack); setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) + auto start = std::chrono::high_resolution_clock::now(); + try { if(stack->creatureId() == CreatureID::CATAPULT) @@ -276,6 +285,8 @@ void CBattleAI::activeStack( const CStack * stack ) attemptCastingSpell(); + logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); + if(cb->battleIsFinished() || !stack->alive()) { //spellcast may finish battle or kill active stack @@ -312,6 +323,8 @@ void CBattleAI::activeStack( const CStack * stack ) movesSkippedByDefense = 0; } + logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); + cb->battleMakeUnitAction(result); } From ba9998ac660ca01574139a8e074cbb864807163c Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Tue, 8 Aug 2023 18:38:41 +0300 Subject: [PATCH 146/168] BattleAI: fast targets optimization --- AI/BattleAI/BattleAI.cpp | 7 ++- AI/BattleAI/BattleExchangeVariant.cpp | 6 ++- AI/BattleAI/PotentialTargets.cpp | 11 ----- lib/spells/BattleSpellMechanics.cpp | 67 +++++++++++++++++++++++---- lib/spells/BattleSpellMechanics.h | 2 +- lib/spells/ISpellMechanics.cpp | 6 +-- lib/spells/ISpellMechanics.h | 4 +- test/mock/mock_spells_Mechanics.h | 2 +- 8 files changed, 74 insertions(+), 31 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 01947d4d5..0f7068402 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -507,7 +507,12 @@ void CBattleAI::attemptCastingSpell() { spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell); - for(auto & target : temp.findPotentialTargets()) + if(!spell->isDamage() && spell->getTargetType() == spells::AimType::LOCATION) + continue; + + const bool FAST = true; + + for(auto & target : temp.findPotentialTargets(FAST)) { PossibleSpellcast ps; ps.dest = target; diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index a1a4ddc24..d796134cd 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -600,6 +600,8 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb) if(unit->isTurret()) continue; + auto unitSpeed = unit->speed(turn); + if(turnBattle.battleCanShoot(unit)) { for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) @@ -614,7 +616,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb) for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) { - bool reachable = unitReachability.distances[hex] <= unit->speed(turn); + bool reachable = unitReachability.distances[hex] <= unitSpeed; if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) { @@ -624,7 +626,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb) { for(BattleHex neighbor : hex.neighbouringTiles()) { - reachable = unitReachability.distances[neighbor] <= unit->speed(turn); + reachable = unitReachability.distances[neighbor] <= unitSpeed; if(reachable) break; } diff --git a/AI/BattleAI/PotentialTargets.cpp b/AI/BattleAI/PotentialTargets.cpp index d55e1dfa8..268350b5d 100644 --- a/AI/BattleAI/PotentialTargets.cpp +++ b/AI/BattleAI/PotentialTargets.cpp @@ -84,17 +84,6 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet { return lhs.damageDiff() > rhs.damageDiff(); }); - - if (!possibleAttacks.empty()) - { - auto & bestAp = possibleAttacks[0]; - - logGlobal->debug("Battle AI best: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld", - bestAp.attack.attacker->unitType()->getJsonKey(), - state.battleGetUnitByPos(bestAp.dest)->unitType()->getJsonKey(), - (int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(), - bestAp.defenderDamageReduce, bestAp.attackerDamageReduce, bestAp.collateralDamageReduce, bestAp.shootersBlockedDmg); - } } int64_t PotentialTargets::bestActionValue() const diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index a09cf1965..82a16a276 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -590,7 +590,7 @@ std::vector BattleSpellMechanics::getTargetTypes() const return ret; } -std::vector BattleSpellMechanics::getPossibleDestinations(size_t index, AimType aimType, const Target & current) const +std::vector BattleSpellMechanics::getPossibleDestinations(size_t index, AimType aimType, const Target & current, bool fast) const { //TODO: BattleSpellMechanics::getPossibleDestinations @@ -602,19 +602,66 @@ std::vector BattleSpellMechanics::getPossibleDestinations(size_t in switch(aimType) { case AimType::CREATURE: - case AimType::LOCATION: - for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) + { + auto stacks = battle()->battleGetAllStacks(); + + for(auto stack : stacks) { - BattleHex dest(i); - if(dest.isAvailable()) + Target tmp = current; + tmp.emplace_back(stack->getPosition()); + + detail::ProblemImpl ignored; + + if(canBeCastAt(tmp, ignored)) + ret.emplace_back(stack->getPosition()); + } + + break; + } + + case AimType::LOCATION: + if(fast) + { + auto stacks = battle()->battleGetAllStacks(); + std::set hexesToCheck; + + for(auto stack : stacks) { - Target tmp = current; - tmp.emplace_back(dest); + hexesToCheck.insert(stack->getPosition()); - detail::ProblemImpl ignored; + for(auto adjacent : stack->getPosition().neighbouringTiles()) + hexesToCheck.insert(adjacent); + } - if(canBeCastAt(tmp, ignored)) - ret.emplace_back(dest); + for(auto hex : hexesToCheck) + { + if(hex.isAvailable()) + { + Target tmp = current; + tmp.emplace_back(hex); + + detail::ProblemImpl ignored; + + if(canBeCastAt(tmp, ignored)) + ret.emplace_back(hex); + } + } + } + else + { + for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) + { + BattleHex dest(i); + if(dest.isAvailable()) + { + Target tmp = current; + tmp.emplace_back(dest); + + detail::ProblemImpl ignored; + + if(canBeCastAt(tmp, ignored)) + ret.emplace_back(dest); + } } } break; diff --git a/lib/spells/BattleSpellMechanics.h b/lib/spells/BattleSpellMechanics.h index 7909da805..b89f1f444 100644 --- a/lib/spells/BattleSpellMechanics.h +++ b/lib/spells/BattleSpellMechanics.h @@ -50,7 +50,7 @@ public: /// Returns vector of all possible destinations for specified aim type /// index - ??? /// current - ??? - std::vector getPossibleDestinations(size_t index, AimType aimType, const Target & current) const override final; + std::vector getPossibleDestinations(size_t index, AimType aimType, const Target & current, bool fast) const override final; /// Returns true if spell can be cast on unit bool isReceptive(const battle::Unit * target) const override; diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index 4f9d111be..f8709b882 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -326,7 +326,7 @@ bool BattleCast::castIfPossible(ServerCallback * server, Target target) return false; } -std::vector BattleCast::findPotentialTargets() const +std::vector BattleCast::findPotentialTargets(bool fast) const { //TODO: for more than 2 destinations per target much more efficient algorithm is required @@ -354,7 +354,7 @@ std::vector BattleCast::findPotentialTargets() const if(previous.empty()) { Target empty; - destinations = m->getPossibleDestinations(index, targetTypes.at(index), empty); + destinations = m->getPossibleDestinations(index, targetTypes.at(index), empty, fast); for(auto & destination : destinations) { @@ -367,7 +367,7 @@ std::vector BattleCast::findPotentialTargets() const { for(const Target & current : previous) { - destinations = m->getPossibleDestinations(index, targetTypes.at(index), current); + destinations = m->getPossibleDestinations(index, targetTypes.at(index), current, fast); for(auto & destination : destinations) { diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index 2abce5b7b..e540bf20d 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -139,7 +139,7 @@ public: ///cast with silent check for permitted cast bool castIfPossible(ServerCallback * server, Target target); - std::vector findPotentialTargets() const; + std::vector findPotentialTargets(bool fast = false) const; private: ///spell school level @@ -199,7 +199,7 @@ public: virtual std::vector getTargetTypes() const = 0; - virtual std::vector getPossibleDestinations(size_t index, AimType aimType, const Target & current) const = 0; + virtual std::vector getPossibleDestinations(size_t index, AimType aimType, const Target & current, bool fast = false) const = 0; virtual const Spell * getSpell() const = 0; diff --git a/test/mock/mock_spells_Mechanics.h b/test/mock/mock_spells_Mechanics.h index 05bc11118..e3efe9950 100644 --- a/test/mock/mock_spells_Mechanics.h +++ b/test/mock/mock_spells_Mechanics.h @@ -34,7 +34,7 @@ public: MOCK_CONST_METHOD1(isReceptive, bool(const battle::Unit * )); MOCK_CONST_METHOD0(getTargetTypes, std::vector()); - MOCK_CONST_METHOD3(getPossibleDestinations, std::vector(size_t, AimType, const Target &)); + MOCK_CONST_METHOD4(getPossibleDestinations, std::vector(size_t, AimType, const Target &, bool)); MOCK_CONST_METHOD0(getSpell, const Spell *()); From dc2135da7f2d803a3f7adbc0af823678fdafbf47 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 9 Aug 2023 00:46:55 +0300 Subject: [PATCH 147/168] Fix potential access to empty std function on hero vs hero combat --- client/battle/BattleInterfaceClasses.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index a6289d07f..7f2f0647d 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -636,7 +636,9 @@ void BattleResultWindow::show(Canvas & to) void BattleResultWindow::buttonPressed(int button) { - resultCallback(button); + if (resultCallback) + resultCallback(button); + CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon close(); From f483f7bb53eb9c69f4c57b1616e295f1506e1a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 10 Aug 2023 11:52:31 +0200 Subject: [PATCH 148/168] Fix crash at battle end --- lib/NetPacksLib.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index db8a2dd8d..9db793995 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2265,6 +2265,9 @@ void BattleAttack::applyGs(CGameState * gs) void StartAction::applyGs(CGameState *gs) { + if (!gs->curB) + return; + CStack *st = gs->curB->getStack(ba.stackNumber); if(ba.actionType == EActionType::END_TACTIC_PHASE) From e9fb0c9b8c39d14d8afd08024bd9d28fa4021e2f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 10 Aug 2023 12:54:26 +0300 Subject: [PATCH 149/168] Make object fade-out / fade-in instant on instant movement speed --- client/mapView/MapViewController.cpp | 24 ++++++++++++++++++++---- client/mapView/MapViewController.h | 1 + 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 7f29987f7..09f6589d6 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -247,6 +247,20 @@ void MapViewController::afterRender() } } +bool MapViewController::isEventInstant(const CGObjectInstance * obj) +{ + if (!isEventVisible(obj)) + return true; + + if(!LOCPLINT->makingTurn && settings["adventure"]["enemyMoveTime"].Float() <= 0) + return true; // instant movement speed + + if(LOCPLINT->makingTurn && settings["adventure"]["heroMoveTime"].Float() <= 0) + return true; // instant movement speed + + return false; +} + bool MapViewController::isEventVisible(const CGObjectInstance * obj) { if(adventureContext == nullptr) @@ -358,7 +372,8 @@ void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int { if(isEventVisible(obj, from, dest)) { - fadeOutObject(obj); + if (!isEventInstant(obj)) + fadeOutObject(obj); setViewCenter(obj->getSightCenter()); } else @@ -381,7 +396,8 @@ void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const i { if(isEventVisible(obj, from, dest)) { - fadeInObject(obj); + if (!isEventInstant(obj)) + fadeInObject(obj); setViewCenter(obj->getSightCenter()); } addObject(obj); @@ -391,7 +407,7 @@ void MapViewController::onObjectFadeIn(const CGObjectInstance * obj) { assert(!hasOngoingAnimations()); - if(isEventVisible(obj)) + if(isEventVisible(obj) && !isEventInstant(obj) ) fadeInObject(obj); addObject(obj); @@ -401,7 +417,7 @@ void MapViewController::onObjectFadeOut(const CGObjectInstance * obj) { assert(!hasOngoingAnimations()); - if(isEventVisible(obj)) + if(isEventVisible(obj) && !isEventInstant(obj) ) fadeOutObject(obj); else removeObject(obj); diff --git a/client/mapView/MapViewController.h b/client/mapView/MapViewController.h index 7b9941deb..bc67e90de 100644 --- a/client/mapView/MapViewController.h +++ b/client/mapView/MapViewController.h @@ -50,6 +50,7 @@ class MapViewController : public IMapObjectObserver std::shared_ptr puzzleMapContext; private: + bool isEventInstant(const CGObjectInstance * obj); bool isEventVisible(const CGObjectInstance * obj); bool isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest); From 84436f7f18a366d8c4d07131fbb5a909a09822ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 10 Aug 2023 18:29:49 +0200 Subject: [PATCH 150/168] Throw runtime error explicitely --- lib/NetPacksLib.cpp | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 9db793995..97187c890 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -38,6 +38,8 @@ VCMI_LIB_NAMESPACE_BEGIN +#define THROW_IF_NO_BATTLE if (!gs->curB) throw std::runtime_error("Trying to apply pack when no battle!"); + void CPack::visit(ICPackVisitor & visitor) { visitBasic(visitor); @@ -2127,16 +2129,19 @@ void BattleStart::applyGs(CGameState * gs) const void BattleNextRound::applyGs(CGameState * gs) const { + THROW_IF_NO_BATTLE gs->curB->nextRound(round); } void BattleSetActiveStack::applyGs(CGameState * gs) const { + THROW_IF_NO_BATTLE gs->curB->nextTurn(stack); } void BattleTriggerEffect::applyGs(CGameState * gs) const { + THROW_IF_NO_BATTLE CStack * st = gs->curB->getStack(stackID); assert(st); switch(static_cast(effect)) @@ -2232,6 +2237,7 @@ void BattleLogMessage::applyBattle(IBattleState * battleState) void BattleStackMoved::applyGs(CGameState *gs) { + THROW_IF_NO_BATTLE applyBattle(gs->curB); } @@ -2242,6 +2248,7 @@ void BattleStackMoved::applyBattle(IBattleState * battleState) void BattleStackAttacked::applyGs(CGameState * gs) { + THROW_IF_NO_BATTLE applyBattle(gs->curB); } @@ -2252,6 +2259,7 @@ void BattleStackAttacked::applyBattle(IBattleState * battleState) void BattleAttack::applyGs(CGameState * gs) { + THROW_IF_NO_BATTLE CStack * attacker = gs->curB->getStack(stackAttacking); assert(attacker); @@ -2265,8 +2273,7 @@ void BattleAttack::applyGs(CGameState * gs) void StartAction::applyGs(CGameState *gs) { - if (!gs->curB) - return; + THROW_IF_NO_BATTLE CStack *st = gs->curB->getStack(ba.stackNumber); @@ -2316,7 +2323,7 @@ void StartAction::applyGs(CGameState *gs) void BattleSpellCast::applyGs(CGameState * gs) const { - assert(gs->curB); + THROW_IF_NO_BATTLE if(castByHero) { @@ -2329,6 +2336,7 @@ void BattleSpellCast::applyGs(CGameState * gs) const void SetStackEffect::applyGs(CGameState *gs) { + THROW_IF_NO_BATTLE applyBattle(gs->curB); } @@ -2347,6 +2355,7 @@ void SetStackEffect::applyBattle(IBattleState * battleState) void StacksInjured::applyGs(CGameState *gs) { + THROW_IF_NO_BATTLE applyBattle(gs->curB); } @@ -2358,6 +2367,7 @@ void StacksInjured::applyBattle(IBattleState * battleState) void BattleUnitsChanged::applyGs(CGameState *gs) { + THROW_IF_NO_BATTLE applyBattle(gs->curB); } @@ -2388,8 +2398,8 @@ void BattleUnitsChanged::applyBattle(IBattleState * battleState) void BattleObstaclesChanged::applyGs(CGameState * gs) { - if(gs->curB) - applyBattle(gs->curB); + THROW_IF_NO_BATTLE; + applyBattle(gs->curB); } void BattleObstaclesChanged::applyBattle(IBattleState * battleState) @@ -2420,8 +2430,8 @@ CatapultAttack::~CatapultAttack() = default; void CatapultAttack::applyGs(CGameState * gs) { - if(gs->curB) - applyBattle(gs->curB); + THROW_IF_NO_BATTLE + applyBattle(gs->curB); } void CatapultAttack::visitTyped(ICPackVisitor & visitor) @@ -2447,6 +2457,7 @@ void CatapultAttack::applyBattle(IBattleState * battleState) void BattleSetStackProperty::applyGs(CGameState * gs) const { + THROW_IF_NO_BATTLE CStack * stack = gs->curB->getStack(stackID); switch(which) { From 0252d0f98631d9d151dc7661de7a21a1c38f7347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Fri, 11 Aug 2023 07:45:24 +0200 Subject: [PATCH 151/168] Fix hota offset + 2 possible crashes --- lib/rmg/RmgObject.cpp | 10 ------ lib/rmg/modificators/ObjectManager.cpp | 49 +++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 0106bee4f..540463948 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -344,16 +344,6 @@ void Object::Instance::finalize(RmgMap & map) setTemplate(terrainType->getId()); } } - if (dObject.ID == Obj::MONSTER) - { - //Make up for extra offset in HotA creature templates - auto visitableOffset = dObject.getVisitableOffset(); - auto fixedPos = getPosition(true) + visitableOffset; - vstd::abetween(fixedPos.x, visitableOffset.x, map.width() - 1); - vstd::abetween(fixedPos.y, visitableOffset.y, map.height() - 1); - int3 parentPos = getPosition(true) - getPosition(false); - setPosition(fixedPos - parentPos); - } if (dObject.isVisitable() && !map.isOnMap(dObject.visitablePos())) throw rmgException(boost::to_string(boost::format("Visitable tile %s of object %d at %s is outside the map") % dObject.visitablePos().toString() % dObject.id % dObject.pos.toString())); diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 46f001c90..6644ee93f 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -129,6 +129,19 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object { float bestWeight = 0.f; int3 result(-1, -1, -1); + + //Blocked area might not cover object position if it has an offset from (0,0) + auto outsideTheMap = [this, &obj]() -> bool + { + for (const auto& oi : obj.instances()) + { + if (!map.isOnMap(oi->getPosition(true))) + { + return true; + } + } + return false; + }; if(optimizer & OptimizeType::DISTANCE) { @@ -149,6 +162,9 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object if(!searchArea.contains(obj.getArea()) || !searchArea.overlap(obj.getAccessibleArea())) continue; + + if (outsideTheMap()) + continue; float weight = weightFunction(tile); if(weight > bestWeight) @@ -168,9 +184,12 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object if (obj.getVisibleTop().y < 0) continue; - + if(!searchArea.contains(obj.getArea()) || !searchArea.overlap(obj.getAccessibleArea())) continue; + + if (outsideTheMap()) + continue; float weight = weightFunction(tile); if(weight > bestWeight) @@ -416,7 +435,7 @@ bool ObjectManager::createRequiredObjects() //create object on specific positions //TODO: implement guards - for (const auto &objInfo : instantObjects) + for (const auto &objInfo : instantObjects) //Unused ATM { rmg::Object rmgObject(*objInfo.obj); rmgObject.setPosition(objInfo.pos); @@ -435,6 +454,21 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD { object.finalize(map); + if (object.instances().size() == 1 && object.instances().front()->object().ID == Obj::MONSTER) + { + //Fix for HoTA offset - lonely guards + object.getPosition(); + auto monster = object.instances().front(); + auto visitableOffset = monster->object().getVisitableOffset(); + auto fixedPos = monster->getPosition(true) + visitableOffset; + + //Do not place guard outside the map + vstd::abetween(fixedPos.x, visitableOffset.x, map.width() - 1); + vstd::abetween(fixedPos.y, visitableOffset.y, map.height() - 1); + int3 parentOffset = monster->getPosition(true) - monster->getPosition(false); + monster->setPosition(fixedPos - parentOffset); + } + Zone::Lock lock(zone.areaMutex); zone.areaPossible().subtract(object.getArea()); bool keepVisitable = zone.freePaths().contains(object.getVisitablePosition()); @@ -443,8 +477,8 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD zone.freePaths().add(object.getVisitablePosition()); zone.areaUsed().unite(object.getArea()); zone.areaUsed().erase(object.getVisitablePosition()); - - if(guarded) + + if(guarded) //We assume the monster won't be guarded { auto guardedArea = object.instances().back()->getAccessibleArea(); guardedArea.add(object.instances().back()->getVisitablePosition()); @@ -501,6 +535,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD { case Obj::RANDOM_TREASURE_ART: case Obj::RANDOM_MINOR_ART: //In OH3 quest artifacts have higher value than normal arts + case Obj::RANDOM_RESOURCE: { if (auto * qap = zone.getModificator()) { @@ -629,8 +664,12 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard }); auto & instance = object.addInstance(*guard); - instance.setPosition(guardPos - object.getPosition()); instance.setAnyTemplate(); //terrain is irrelevant for monsters, but monsters need some template now + + //Fix HoTA monsters with offset template + auto visitableOffset = instance.object().getVisitableOffset(); + auto fixedPos = guardPos - object.getPosition() + visitableOffset; + instance.setPosition(fixedPos); return true; } From 213eda5e1ee84e0eb7345650e5343f274cce3ca5 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:19:06 +0300 Subject: [PATCH 152/168] altar sacrifice all fix --- client/windows/CTradeWindow.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 6787308fd..7787ec4de 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -1262,11 +1262,14 @@ void CAltarWindow::SacrificeAll() } else { - for(const auto & aw : arts->visibleArtSet.artifactsWorn) + std::vector> artsForMove; + for(const auto& slotInfo : arts->visibleArtSet.artifactsWorn) { - if(!aw.second.locked) - moveArtToAltar(nullptr, aw.second.artifact); + if(!slotInfo.second.locked && slotInfo.second.artifact->artType->isTradable()) + artsForMove.push_back(slotInfo.second.artifact); } + for(auto artInst : artsForMove) + moveArtToAltar(nullptr, artInst); arts->updateWornSlots(); SacrificeBackpack(); } From 1b531c67f5b4d4ffd11b5f0d92b4b90fff5dc088 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 11 Aug 2023 15:06:02 +0300 Subject: [PATCH 153/168] Added option to configure reserved screen area on mobile platforms --- launcher/settingsView/csettingsview_moc.cpp | 16 +- launcher/settingsView/csettingsview_moc.h | 2 + launcher/settingsView/csettingsview_moc.ui | 911 ++++++++++---------- 3 files changed, 482 insertions(+), 447 deletions(-) diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index c4bbb9eec..9f00a369f 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -72,6 +72,8 @@ void CSettingsView::loadSettings() ui->comboBoxFullScreen->hide(); ui->labelFullScreen->hide(); #else + ui->labelReservedArea->hide(); + ui->spinBoxReservedArea->hide(); if (settings["video"]["realFullscreen"].Bool()) ui->comboBoxFullScreen->setCurrentIndex(2); else @@ -81,6 +83,7 @@ void CSettingsView::loadSettings() ui->spinBoxInterfaceScaling->setValue(settings["video"]["resolution"]["scaling"].Float()); ui->spinBoxFramerateLimit->setValue(settings["video"]["targetfps"].Float()); + ui->spinBoxReservedArea->setValue(std::round(settings["video"]["reservedWidth"].Float() * 100)); ui->comboBoxFriendlyAI->setCurrentText(QString::fromStdString(settings["server"]["friendlyAI"].String())); ui->comboBoxNeutralAI->setCurrentText(QString::fromStdString(settings["server"]["neutralAI"].String())); @@ -468,14 +471,12 @@ void CSettingsView::on_lineEditRepositoryExtra_textEdited(const QString &arg1) node->String() = arg1.toStdString(); } - void CSettingsView::on_spinBoxInterfaceScaling_valueChanged(int arg1) { Settings node = settings.write["video"]["resolution"]["scaling"]; node->Float() = arg1; } - void CSettingsView::on_refreshRepositoriesButton_clicked() { auto * mainWindow = dynamic_cast(qApp->activeWindow()); @@ -487,7 +488,6 @@ void CSettingsView::on_refreshRepositoriesButton_clicked() mainWindow->getModView()->loadRepositories(); } - void CSettingsView::on_spinBoxFramerateLimit_valueChanged(int arg1) { Settings node = settings.write["video"]["targetfps"]; @@ -500,14 +500,12 @@ void CSettingsView::on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &a node->String() = arg1.toUtf8().data(); } - void CSettingsView::on_comboBoxAlliedPlayerAI_currentTextChanged(const QString &arg1) { Settings node = settings.write["server"]["alliedAI"]; node->String() = arg1.toUtf8().data(); } - void CSettingsView::on_checkBoxAutoSavePrefix_stateChanged(int arg1) { Settings node = settings.write["general"]["useSavePrefix"]; @@ -515,17 +513,21 @@ void CSettingsView::on_checkBoxAutoSavePrefix_stateChanged(int arg1) ui->lineEditAutoSavePrefix->setEnabled(arg1); } - void CSettingsView::on_spinBoxAutoSaveLimit_valueChanged(int arg1) { Settings node = settings.write["general"]["autosaveCountLimit"]; node->Float() = arg1; } - void CSettingsView::on_lineEditAutoSavePrefix_textEdited(const QString &arg1) { Settings node = settings.write["general"]["savePrefix"]; node->String() = arg1.toStdString(); } +void CSettingsView::on_spinBoxReservedArea_valueChanged(int arg1) +{ + Settings node = settings.write["video"]["reservedWidth"]; + node->Float() = float(arg1) / 100; // percentage -> ratio +} + diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 6e27d9896..5cc14c505 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -72,6 +72,8 @@ private slots: void on_lineEditAutoSavePrefix_textEdited(const QString &arg1); + void on_spinBoxReservedArea_valueChanged(int arg1); + private: Ui::CSettingsView * ui; diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui index 5e3f43e84..5924e4f70 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -42,6 +42,7 @@ + 75 true @@ -106,35 +107,136 @@ 0 - 0 + -197 610 - 790 + 768 - - + + + + + + + + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + Display index + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + Autosave limit (0 = off) + + + + + + + Fullscreen + + + + + + + Network port + + + + + + 75 true - Artificial Intelligence + General - - + + - + Autosave - + + + + + + + Hardware + + + + + Software + + + + + + + + Show intro + + + + + + + + + true - + true @@ -147,13 +249,358 @@ - - + + + + 20 + + + 1000 + + + 10 + + + + + + + + + + Framerate Limit + + + + + + + + + + Adventure Map Enemies + + + + + + + Default repository + + + + + + + + + + 1 + + + + Off + + + + + On + + + + + + + + + 75 + true + + + + Video + + + + + + + Autosave prefix + + + + + + + Resolution + + + + + + + + + + + + + + + + + true + + + + + + + Cursor + + + + + + + 1 + + + + Off + + + + + On + + + + + + + + Refresh now + + + + + - - true + + + + + + Heroes III Translation + + + + + + + 1 + + + + Off + + + + + On + + + + + + + + Enemy AI in battles + + + + + + + + 75 + true + + + + Mod Repositories + + + + + + + Additional repository + + + + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + 50 + + + 400 + + + 10 + + + + + + + Heroes III Data Language + + + + + + + + + + Neutral AI in battles + + + + + + + Interface Scaling + + + + + + + 1024 + + + 65535 + + + 3030 + + + + + + + + + + + + + + Friendly AI in battles + + + + + + + VCMI Language + + + + + + + + + + Check on startup + + + + + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + Adventure Map Allies + + + + + + + + 75 + true + + + + Artificial Intelligence + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + empty = map name prefix @@ -188,445 +635,29 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Resolution - - - - - - - Adventure Map Allies - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - - true - - - - Mod Repositories - - - - - - - 1 - - - - Off - - - - - On - - - - - - - - - - - - - - - Default repository - - - - - - - Additional repository - - - - - - - Neutral AI in battles - - - - - - - + Reserved screen area - + + + % + - 50 + 0 - 400 + 25 - 10 - - - - - - - Interface Scaling - - - - - - - Heroes III Data Language - - - - - - - Autosave limit (0 = off) - - - - - - - - - - Friendly AI in battles - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - Display index - - - - - - - Adventure Map Enemies - - - - - - - 20 - - - 1000 - - - 10 - - - - - - - Show intro - - - - - - 1 - - - Off - - - - - On - - - - - - - - - - - - Hardware - - - - - Software - - - - - - - - Fullscreen - - - - - - - Cursor - - - - - - - - - - - - - - Enemy AI in battles - - - - - - - Check on startup - - - - - - - - true - - - - General - - - - - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - Refresh now - - - - - - - 1024 - - - 65535 - - 3030 - - - - - - - Network port - - - - - - - Heroes III Translation - - - - - - - VCMI Language - - - - - - - Autosave - - - - - - - - true - - - - Video - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - - - - Framerate Limit - - - - - - - 1 - - - - Off - - - - - On - - - - - - - - Autosave prefix - - - - - - - - - - - - - - - - - empty = map name prefix + 0 From c0e51fc13e270e21bdcf2ddbbe5f1bd530a7fb5f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 11 Aug 2023 15:08:22 +0300 Subject: [PATCH 154/168] Update translations --- launcher/settingsView/csettingsview_moc.ui | 2 +- launcher/translation/chinese.ts | 103 +++++++++++---------- launcher/translation/english.ts | 103 +++++++++++---------- launcher/translation/french.ts | 103 +++++++++++---------- launcher/translation/german.ts | 103 +++++++++++---------- launcher/translation/polish.ts | 103 +++++++++++---------- launcher/translation/russian.ts | 103 +++++++++++---------- launcher/translation/spanish.ts | 103 +++++++++++---------- launcher/translation/ukrainian.ts | 103 +++++++++++---------- 9 files changed, 433 insertions(+), 393 deletions(-) diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui index 5924e4f70..186689a6c 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -645,7 +645,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - % + % 0 diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 8ac241e96..6bdd419ce 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -407,123 +407,123 @@ CSettingsView - - - + + + Off 关闭 - - + + Artificial Intelligence 人工智能 - - + + Mod Repositories 模组仓库 - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + On 开启 - + Cursor 鼠标指针 - + Heroes III Data Language 英雄无敌3数据语言 - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -534,95 +534,100 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + + Reserved screen area + + + + Hardware 硬件 - + Software 软件 - + Heroes III Translation 发布版本里找不到这个项,不太清楚意义 英雄无敌3翻译 - + Check on startup 启动时检查更新 - + Fullscreen 全屏 - - + + General 通用设置 - + VCMI Language VCMI语言 - + Resolution 分辨率 - + Autosave 自动存档 - + Display index 显示器序号 - + Network port 网络端口 - - + + Video 视频设置 - + Show intro 显示开场动画 - + Active 激活 - + Disabled 禁用 - + Enable 启用 - + Not Installed 未安装 - + Install 安装 diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 9270e252a..4d6ad1deb 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -406,123 +406,123 @@ CSettingsView - - - + + + Off - - + + Artificial Intelligence - - + + Mod Repositories - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + On - + Cursor - + Heroes III Data Language - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -533,94 +533,99 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + + Reserved screen area + + + + Hardware - + Software - + Heroes III Translation - + Check on startup - + Fullscreen - - + + General - + VCMI Language - + Resolution - + Autosave - + Display index - + Network port - - + + Video - + Show intro - + Active - + Disabled - + Enable - + Not Installed - + Install diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index 110fbcbbd..e59b15e82 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -411,43 +411,43 @@ CSettingsView - - - + + + Off Désactivé - - + + Artificial Intelligence Intelligence Artificielle - - + + Mod Repositories Dépôts de Mod - - - + + + On Activé - + Enemy AI in battles IA ennemie dans les batailles - + Default repository Dépôt par défaut - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -464,174 +464,179 @@ Mode fenêtré sans bord - le jeu s"exécutera dans une fenêtre qui couvre Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écran et utilisera la résolution sélectionnée. - + Windowed Fenêtré - + Borderless fullscreen Fenêtré sans bord - + Exclusive fullscreen Plein écran exclusif - + + Reserved screen area + + + + Neutral AI in battles IA neutre dans les batailles - + Autosave limit (0 = off) - + Adventure Map Enemies Ennemis de la carte d"aventure - + Autosave prefix - + empty = map name prefix - + Interface Scaling Mise à l"échelle de l"interface - + Cursor Curseur - + Heroes III Data Language Langue des Données de Heroes III - + Framerate Limit Limite de fréquence d"images - + Hardware Matériel - + Software Logiciel - + Heroes III Translation Traduction de Heroes III - + Adventure Map Allies Alliés de la carte d"aventure - + Additional repository Dépôt supplémentaire - + Check on startup Vérifier au démarrage - + Refresh now Actualiser maintenant - + Friendly AI in battles IA amicale dans les batailles - + Fullscreen Plein écran - - + + General Général - + VCMI Language Langue de VCMI - + Resolution Résolution - + Autosave Sauvegarde automatique - + Display index Index d'affichage - + Network port Port de réseau - - + + Video Vidéo - + Show intro Montrer l'intro - + Active Actif - + Disabled Désactivé - + Enable Activé - + Not Installed Pas Installé - + Install Installer diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index 2840fb21f..fd803264f 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -406,123 +406,123 @@ CSettingsView - - - + + + Off Aus - - + + Artificial Intelligence Künstliche Intelligenz - - + + Mod Repositories Mod-Repositorien - + Interface Scaling Skalierung der Benutzeroberfläche - + Neutral AI in battles Neutrale KI in Kämpfen - + Enemy AI in battles Gegnerische KI in Kämpfen - + Additional repository Zusätzliches Repository - + Adventure Map Allies Abenteuerkarte Verbündete - + Adventure Map Enemies Abenteuerkarte Feinde - + Windowed Fenstermodus - + Borderless fullscreen Randloser Vollbildmodus - + Exclusive fullscreen Exklusiver Vollbildmodus - + Autosave limit (0 = off) Limit für Autospeicherung (0 = aus) - + Friendly AI in battles Freundliche KI in Kämpfen - + Framerate Limit Limit der Bildrate - + Autosave prefix Präfix für Autospeicherung - + empty = map name prefix leer = Kartenname als Präfix - + Refresh now Jetzt aktualisieren - + Default repository Standard Repository - - - + + + On An - + Cursor Zeiger - + Heroes III Data Language Sprache der Heroes III Daten - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -539,94 +539,99 @@ Randloser Fenstermodus - das Spiel läuft in einem Fenster, das den gesamten Bil Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwendet die gewählte Auflösung. - + + Reserved screen area + + + + Hardware Hardware - + Software Software - + Heroes III Translation Heroes III Übersetzung - + Check on startup Beim Start prüfen - + Fullscreen Vollbild - - + + General Allgemein - + VCMI Language VCMI-Sprache - + Resolution Auflösung - + Autosave Autospeichern - + Display index Anzeige-Index - + Network port Netzwerk-Port - - + + Video Video - + Show intro Intro anzeigen - + Active Aktiv - + Disabled Deaktiviert - + Enable Aktivieren - + Not Installed Nicht installiert - + Install Installieren diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index d72e7c62e..75ea0498f 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -406,123 +406,123 @@ CSettingsView - - - + + + Off Wyłączony - - + + Artificial Intelligence Sztuczna Inteligencja - - + + Mod Repositories Repozytoria modów - + Interface Scaling Skala interfejsu - + Neutral AI in battles AI bitewne jednostek neutralnych - + Enemy AI in battles AI bitewne wrogów - + Additional repository Dodatkowe repozytorium - + Adventure Map Allies AI sojuszników mapy przygody - + Adventure Map Enemies AI wrogów mapy przygody - + Windowed Okno - + Borderless fullscreen Pełny ekran (tryb okna) - + Exclusive fullscreen Pełny ekran klasyczny - + Autosave limit (0 = off) Limit autozapisów (0 = brak) - + Friendly AI in battles AI bitewne sojuszników - + Framerate Limit Limit FPS - + Autosave prefix Przedrostek autozapisu - + empty = map name prefix puste = przedrostek z nazwy mapy - + Refresh now Odśwież - + Default repository Domyślne repozytorium - - - + + + On Włączony - + Cursor Kursor - + Heroes III Data Language Język plików Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -539,94 +539,99 @@ Pełny ekran w trybie okna - gra uruchomi się w oknie przysłaniającym cały e Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybranej przez ciebie rozdzielczości ekranu. - + + Reserved screen area + + + + Hardware Sprzętowy - + Software Programowy - + Heroes III Translation Tłumaczenie Heroes III - + Check on startup Sprawdzaj przy uruchomieniu - + Fullscreen Pełny ekran - - + + General Ogólne - + VCMI Language Język VCMI - + Resolution Rozdzielczość - + Autosave Autozapis - + Display index Numer wyświetlacza - + Network port Port sieciowy - - + + Video Obraz - + Show intro Pokaż intro - + Active Aktywny - + Disabled Wyłączone - + Enable Włącz - + Not Installed Nie zainstalowano - + Install Zainstaluj diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index 7e0953a29..a5ef0dd5c 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -406,144 +406,149 @@ CSettingsView - + Interface Scaling - - - + + + Off Отключено - - - + + + On Включено - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Check on startup Проверять при запуске - + Fullscreen Полноэкранный режим - - + + General Общее - + VCMI Language Язык VCMI - + Cursor Курсор - - + + Artificial Intelligence Искусственный интеллект - - + + Mod Repositories Репозитории модов - + Adventure Map Allies - + Refresh now - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + + Reserved screen area + + + + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Default repository - + Heroes III Data Language Язык данных Героев III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -554,73 +559,73 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware Аппаратный - + Software Программный - + Heroes III Translation Перевод Героев III - + Resolution Разрешение экрана - + Autosave Автосохранение - + Display index Дисплей - + Network port Сетевой порт - - + + Video Графика - + Show intro Вступление - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index 6401189bc..04499cedf 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -406,165 +406,170 @@ CSettingsView - - - + + + Off Desactivado - - + + Artificial Intelligence Inteligencia Artificial - - + + Mod Repositories Repositorios de Mods - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + On Encendido - + Cursor Cursor - + Heroes III Translation Traducción de Heroes III - + + Reserved screen area + + + + Fullscreen Pantalla completa - - + + General General - + VCMI Language Idioma de VCMI - + Resolution Resolución - + Autosave Autoguardado - + Display index Mostrar índice - + Network port Puerto de red - - + + Video Vídeo - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -575,52 +580,52 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware Hardware - + Software Software - + Show intro Mostrar introducción - + Check on startup Comprovar al inicio - + Heroes III Data Language Idioma de los datos de Heroes III. - + Active Activado - + Disabled Desactivado - + Enable Activar - + Not Installed No Instalado - + Install Instalar diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index b4362d867..d3015e426 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -406,123 +406,123 @@ CSettingsView - - - + + + Off Вимкнено - - + + Artificial Intelligence Штучний інтелект - - + + Mod Repositories Репозиторії модифікацій - + Interface Scaling Масштабування інтерфейсу - + Neutral AI in battles Нейтральний ШІ в боях - + Enemy AI in battles Ворожий ШІ в боях - + Additional repository Додатковий репозиторій - + Adventure Map Allies Союзники на мапі пригод - + Adventure Map Enemies Вороги на мапі пригод - + Windowed У вікні - + Borderless fullscreen Повноекранне вікно - + Exclusive fullscreen Повноекранний (ексклюзивно) - + Autosave limit (0 = off) Кількість автозбережень - + Friendly AI in battles Дружній ШІ в боях - + Framerate Limit Обмеження частоти кадрів - + Autosave prefix Префікс назв автозбережень - + empty = map name prefix (використовувати назву карти) - + Refresh now Оновити зараз - + Default repository Стандартний репозиторій - - - + + + On Увімкнено - + Cursor Курсор - + Heroes III Data Language Мова Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -539,94 +539,99 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Повноекранний ексклюзивний режим - гра займатиме весь екран і використовуватиме вибрану роздільну здатність. - + + Reserved screen area + Зарезервована зона екрану + + + Hardware Апаратний - + Software Програмний - + Heroes III Translation Переклад Heroes III - + Check on startup Перевіряти на старті - + Fullscreen Повноекранний режим - - + + General Загальні налаштування - + VCMI Language Мова VCMI - + Resolution Роздільна здатність - + Autosave Автозбереження - + Display index Дісплей - + Network port Мережевий порт - - + + Video Графіка - + Show intro Вступні відео - + Active Активні - + Disabled Деактивований - + Enable Активувати - + Not Installed Не встановлено - + Install Встановити From 474045194aeb009dba710964bb9e10296db1a521 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 11 Aug 2023 15:59:35 +0300 Subject: [PATCH 155/168] Do not ignore translations for maps & campaigns --- lib/CGeneralTextHandler.cpp | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index eb2cd8900..5739db44f 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -313,19 +313,7 @@ void CGeneralTextHandler::registerStringOverride(const std::string & modContext, assert(!modContext.empty()); assert(!language.empty()); - std::string baseModLanguage = getModLanguage(modContext); - - if (baseModLanguage != language) - { - // this is translation - only add text to existing strings, do not register new ones - if (stringsLocalizations.count(UID.get()) == 0) - { - logMod->warn("Unknown string '%s' in mod '%s' for language '%s'. Ignoring", UID.get(), modContext, language); - return; - } - } - - // NOTE: implicitly creates entry, intended - strings added by vcmi (and potential UI mods) are not registered anywhere at the moment + // NOTE: implicitly creates entry, intended - strings added by maps, campaigns, vcmi and potentially - UI mods are not registered anywhere at the moment auto & entry = stringsLocalizations[UID.get()]; entry.overrideLanguage = language; @@ -349,6 +337,9 @@ bool CGeneralTextHandler::validateTranslation(const std::string & language, cons if (string.second.baseLanguage == language && !string.second.baseValue.empty()) continue; // Base string already uses our language + if (string.second.baseLanguage.empty()) + continue; // String added in localization, not present in base language (e.g. maps/campaigns) + if (config.Struct().count(string.first) > 0) continue; From 775e5948ec4cb2dfb945814319d7004f1354384b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 11 Aug 2023 16:53:02 +0300 Subject: [PATCH 156/168] Preserve chosen campaign bonus on scenario restart request --- server/CVCMIServer.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 8a9ad301e..234ee09ab 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -277,6 +277,12 @@ void CVCMIServer::prepareToRestart() * si = * gh->gs->initialOpts; si->seedToBeUsed = si->seedPostInit = 0; state = EServerState::LOBBY; + if (si->campState) + { + assert(si->campState->currentScenario().has_value()); + campaignMap = si->campState->currentScenario().value_or(CampaignScenarioID(0)); + campaignBonus = si->campState->getBonusID(campaignMap).value_or(-1); + } // FIXME: dirry hack to make sure old CGameHandler::run is finished boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); } From 67b7c39761066c886565264b12948c2267a0a7ce Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 10 Aug 2023 21:18:36 +0300 Subject: [PATCH 157/168] Fix possible nullptr dereference --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 3e1ee23f2..d9bdf3904 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -189,7 +189,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta town->getNameTranslated(), treat.danger, std::to_string(treat.turn), - treat.hero->getNameTranslated()); + treat.hero ? treat.hero->getNameTranslated() : std::string("")); handleCounterAttack(town, treat, treatNode.maximumDanger, tasks); From 942f8bbf058c59aab091455771f0dfbbc3420d98 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 10 Aug 2023 21:18:52 +0300 Subject: [PATCH 158/168] Fix possible access to non-existing building --- lib/mapObjects/CGTownInstance.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 4f088b414..10fd30409 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -996,6 +996,12 @@ CBuilding::TRequired CGTownInstance::genBuildingRequirements(const BuildingID & std::function dependTest = [&](const BuildingID & id) -> CBuilding::TRequired::Variant { + if (town->buildings.count(id) == 0) + { + logMod->error("Invalid building ID %d in building dependencies!", id.getNum()); + return CBuilding::TRequired::OperatorAll(); + } + const CBuilding * build = town->buildings.at(id); CBuilding::TRequired::OperatorAll requirements; From 631e93e846f108195fbe5fd9dae26e4cd0f7ff2b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 10 Aug 2023 21:19:12 +0300 Subject: [PATCH 159/168] Fix missing lock of player interface --- client/gui/CGuiHandler.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index fdccf1517..27c97895a 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -88,6 +88,8 @@ void CGuiHandler::handleEvents() void CGuiHandler::fakeMouseMove() { dispatchMainThread([](){ + assert(CPlayerInterface::pim); + boost::unique_lock lock(*CPlayerInterface::pim); GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition()); }); } From 9d7f46f98507a715fb1c631e9dffb247c3195135 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 10 Aug 2023 21:21:37 +0300 Subject: [PATCH 160/168] Fix crash on right-clicking player flags in RMG UI --- client/lobby/RandomMapTab.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index dfb0157cb..2a0ca914f 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -147,7 +147,6 @@ void RandomMapTab::updateMapInfoByHost() mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels(); // Generate player information - mapInfo->mapHeader->players.clear(); int playersToGen = PlayerColor::PLAYER_LIMIT_I; if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) { @@ -157,10 +156,15 @@ void RandomMapTab::updateMapInfoByHost() playersToGen = mapGenOptions->getPlayerCount(); } - mapInfo->mapHeader->howManyTeams = playersToGen; std::set occupiedTeams; + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + mapInfo->mapHeader->players[i].canComputerPlay = false; + mapInfo->mapHeader->players[i].canHumanPlay = false; + } + for(int i = 0; i < playersToGen; ++i) { PlayerInfo player; @@ -179,7 +183,7 @@ void RandomMapTab::updateMapInfoByHost() occupiedTeams.insert(team); player.hasMainTown = true; player.generateHeroAtMainTown = true; - mapInfo->mapHeader->players.push_back(player); + mapInfo->mapHeader->players[i] = player; } for(auto & player : mapInfo->mapHeader->players) { From ad2bd897d04f71411a0bdb20f4d3176efc460bfb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 10 Aug 2023 21:23:18 +0300 Subject: [PATCH 161/168] Replace boost::format with MetaString Looks like some of H3 localizations don't have string replacements for building / war machine name, resulting in exceptions on formatting attempt. MetaString handles this case silently. --- client/windows/CCastleInterface.cpp | 48 ++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index c5a1327bf..2f6e20f58 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1451,7 +1451,11 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin auto statusbarBackground = std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26); statusbar = CGStatusBar::create(statusbarBackground); - name = std::make_shared(197, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->hcommands[7]) % building->getNameTranslated())); + MetaString nameString; + nameString.appendTextID("core.hallinfo.7"); + nameString.replaceTextID(building->getNameTextID()); + + name = std::make_shared(197, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, nameString.toString()); description = std::make_shared(building->getDescriptionTranslated(), Rect(33, 135, 329, 67), 0, FONT_MEDIUM, ETextAlignment::CENTER); stateText = std::make_shared(getTextForState(state), Rect(33, 216, 329, 67), 0, FONT_SMALL, ETextAlignment::CENTER); @@ -1468,14 +1472,20 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin if(!rightClick) { //normal window - std::string tooltipYes = boost::str(boost::format(CGI->generaltexth->allTexts[595]) % building->getNameTranslated()); - std::string tooltipNo = boost::str(boost::format(CGI->generaltexth->allTexts[596]) % building->getNameTranslated()); - buy = std::make_shared(Point(45, 446), "IBUY30", CButton::tooltip(tooltipYes), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); + MetaString tooltipYes; + tooltipYes.appendTextID("core.genrltxt.595"); + tooltipYes.replaceTextID(building->getNameTextID()); + + MetaString tooltipNo; + tooltipNo.appendTextID("core.genrltxt.596"); + tooltipNo.replaceTextID(building->getNameTextID()); + + buy = std::make_shared(Point(45, 446), "IBUY30", CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); buy->setBorderColor(Colors::METALLIC_GOLD); buy->block(state!=7 || LOCPLINT->playerID != town->tempOwner); - cancel = std::make_shared(Point(290, 445), "ICANCEL", CButton::tooltip(tooltipNo), [&](){ close();}, EShortcut::GLOBAL_CANCEL); + cancel = std::make_shared(Point(290, 445), "ICANCEL", CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL); cancel->setBorderColor(Colors::METALLIC_GOLD); } } @@ -1841,17 +1851,25 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art anim = std::make_shared(64, 50, creature->animDefName); anim->clipRect(113,125,200,150); - title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, - boost::str(boost::format(CGI->generaltexth->allTexts[274]) % creature->getNameSingularTranslated())); + MetaString titleString; + titleString.appendTextID("core.genrltxt.274"); + titleString.replaceTextID(creature->getNameSingularTextID()); + + MetaString buyText; + buyText.appendTextID("core.genrltxt.595"); + buyText.replaceTextID(creature->getNameSingularTextID()); + + MetaString cancelText; + cancelText.appendTextID("core.genrltxt.596"); + cancelText.replaceTextID(creature->getNameSingularTextID()); + + std::string costString = std::to_string(aid.toArtifact(CGI->artifacts())->getPrice()); + + title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); - costValue = std::make_shared(165, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, - std::to_string(aid.toArtifact(CGI->artifacts())->getPrice())); - - std::string text = boost::str(boost::format(CGI->generaltexth->allTexts[595]) % creature->getNameSingularTranslated()); - buy = std::make_shared(Point(42, 312), "IBUY30.DEF", CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); - - text = boost::str(boost::format(CGI->generaltexth->allTexts[596]) % creature->getNameSingularTranslated()); - cancel = std::make_shared(Point(224, 312), "ICANCEL.DEF", CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + costValue = std::make_shared(165, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, costString); + buy = std::make_shared(Point(42, 312), "IBUY30.DEF", CButton::tooltip(buyText.toString()), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(224, 312), "ICANCEL.DEF", CButton::tooltip(cancelText.toString()), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); if(possible) buy->addCallback([=](){ LOCPLINT->cb->buyArtifact(LOCPLINT->cb->getHero(hid),aid); }); From 4b307dc0e4d776fd3941ec92a2fd1ef989c738e4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 11 Aug 2023 18:50:00 +0300 Subject: [PATCH 162/168] More locks to avoid data races on server --- server/CGameHandler.cpp | 2 ++ server/NetPacksServer.cpp | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3cb0f5655..3599c399a 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -6464,6 +6464,8 @@ void CGameHandler::runBattle() bool CGameHandler::makeAutomaticAction(const CStack *stack, BattleAction &ba) { + boost::unique_lock lock(battleActionMutex); + BattleSetActiveStack bsa; bsa.stack = stack->unitId(); bsa.askPlayerInterface = false; diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index f2f37e4a4..7660bc073 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -25,6 +25,8 @@ #include "../lib/spells/ISpellMechanics.h" #include "../lib/serializer/Cast.h" +extern boost::recursive_mutex battleActionMutex; + void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) { gh.save(pack.fname); @@ -280,6 +282,8 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) { + boost::unique_lock lock(battleActionMutex); + const BattleInfo * b = gs.curB; if(!b) gh.throwAndComplain(&pack, "Can not make action - there is no battle ongoing!"); @@ -307,6 +311,8 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack) { + boost::unique_lock lock(battleActionMutex); + const BattleInfo * b = gs.curB; if(!b) gh.throwNotAllowedAction(&pack); From 119d51d1efe6e0a92e3e368c81cb6e5df5c76a96 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:19:06 +0300 Subject: [PATCH 163/168] altar sacrifice all fix --- client/windows/CTradeWindow.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 6787308fd..7787ec4de 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -1262,11 +1262,14 @@ void CAltarWindow::SacrificeAll() } else { - for(const auto & aw : arts->visibleArtSet.artifactsWorn) + std::vector> artsForMove; + for(const auto& slotInfo : arts->visibleArtSet.artifactsWorn) { - if(!aw.second.locked) - moveArtToAltar(nullptr, aw.second.artifact); + if(!slotInfo.second.locked && slotInfo.second.artifact->artType->isTradable()) + artsForMove.push_back(slotInfo.second.artifact); } + for(auto artInst : artsForMove) + moveArtToAltar(nullptr, artInst); arts->updateWornSlots(); SacrificeBackpack(); } From 20e10cd5abfb2c6fc66554098680af07e05ea26f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 9 Aug 2023 00:46:55 +0300 Subject: [PATCH 164/168] Fix potential access to empty std function on hero vs hero combat --- client/battle/BattleInterfaceClasses.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index a6289d07f..7f2f0647d 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -636,7 +636,9 @@ void BattleResultWindow::show(Canvas & to) void BattleResultWindow::buttonPressed(int button) { - resultCallback(button); + if (resultCallback) + resultCallback(button); + CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon close(); From 4ebb49074082abc93921fd273a0727d43f3bcdfd Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 11 Aug 2023 18:53:13 +0300 Subject: [PATCH 165/168] Android build version bump for hotfix --- android/vcmi-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 0a00e9de0..5e67599a8 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,7 +10,7 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 31 - versionCode 1304 + versionCode 1305 versionName "1.3.0" setProperty("archivesBaseName", "vcmi") } From 8f450cf253dea640f3320a972c6ba80e1a8bd4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Fri, 11 Aug 2023 18:43:22 +0200 Subject: [PATCH 166/168] Fix for monsters spawning at left side of the map --- lib/rmg/modificators/ObjectManager.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 6644ee93f..d1afd557e 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -452,13 +452,19 @@ bool ObjectManager::createRequiredObjects() void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateDistance, bool createRoad/* = false*/) { - object.finalize(map); + //object.finalize(map); if (object.instances().size() == 1 && object.instances().front()->object().ID == Obj::MONSTER) { //Fix for HoTA offset - lonely guards - object.getPosition(); + auto monster = object.instances().front(); + if (!monster->object().appearance) + { + //Needed to determine visitable offset + monster->setAnyTemplate(); + } + object.getPosition(); auto visitableOffset = monster->object().getVisitableOffset(); auto fixedPos = monster->getPosition(true) + visitableOffset; @@ -468,6 +474,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD int3 parentOffset = monster->getPosition(true) - monster->getPosition(false); monster->setPosition(fixedPos - parentOffset); } + object.finalize(map); Zone::Lock lock(zone.areaMutex); zone.areaPossible().subtract(object.getArea()); From 4741bd546fd3c10255b4737a5729d1f2c874bad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Fri, 11 Aug 2023 20:13:25 +0200 Subject: [PATCH 167/168] Fix regression --- lib/rmg/modificators/QuestArtifactPlacer.cpp | 10 ++++++---- lib/rmg/threadpool/MapProxy.cpp | 6 ++++++ lib/rmg/threadpool/MapProxy.h | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/rmg/modificators/QuestArtifactPlacer.cpp b/lib/rmg/modificators/QuestArtifactPlacer.cpp index 7640474f3..fd9fe44d8 100644 --- a/lib/rmg/modificators/QuestArtifactPlacer.cpp +++ b/lib/rmg/modificators/QuestArtifactPlacer.cpp @@ -89,14 +89,15 @@ void QuestArtifactPlacer::placeQuestArtifacts(CRandomGenerator & rand) artifactToReplace->getObjectName(), artifactToReplace->getPosition().toString(), VLC->artifacts()->getById(artifactToPlace)->getNameTranslated()); - artifactToReplace->ID = Obj::ARTIFACT; - artifactToReplace->subID = artifactToPlace; //Update appearance. Terrain is irrelevant. auto handler = VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, artifactToPlace); + auto newObj = handler->create(); auto templates = handler->getTemplates(); - artifactToReplace->appearance = templates.front(); - //FIXME: Instance name is still "randomArtifact" + //artifactToReplace->appearance = templates.front(); + newObj->appearance = templates.front(); + newObj->pos = artifactToReplace->pos; + mapProxy->insertObject(newObj); for (auto z : map.getZones()) { @@ -107,6 +108,7 @@ void QuestArtifactPlacer::placeQuestArtifacts(CRandomGenerator & rand) localQap->dropReplacedArtifact(artifactToReplace); } } + mapProxy->removeObject(artifactToReplace); break; } } diff --git a/lib/rmg/threadpool/MapProxy.cpp b/lib/rmg/threadpool/MapProxy.cpp index 7ae73fe9b..d1bf2f8ee 100644 --- a/lib/rmg/threadpool/MapProxy.cpp +++ b/lib/rmg/threadpool/MapProxy.cpp @@ -30,6 +30,12 @@ void MapProxy::insertObjects(std::set& objects) map.getEditManager()->insertObjects(objects); } +void MapProxy::removeObject(CGObjectInstance * obj) +{ + Lock lock(mx); + map.getEditManager()->removeObject(obj); +} + void MapProxy::drawTerrain(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain) { Lock lock(mx); diff --git a/lib/rmg/threadpool/MapProxy.h b/lib/rmg/threadpool/MapProxy.h index c05530d50..a85d74fbf 100644 --- a/lib/rmg/threadpool/MapProxy.h +++ b/lib/rmg/threadpool/MapProxy.h @@ -26,6 +26,7 @@ public: void insertObject(CGObjectInstance * obj); void insertObjects(std::set& objects); + void removeObject(CGObjectInstance* obj); void drawTerrain(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain); void drawRivers(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain); From 626664b8f6ea0bd14a4959ae85c5088a8325cc0d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 12 Aug 2023 14:16:47 +0300 Subject: [PATCH 168/168] Fix possible thread race on accessing GuiHandler from server handler thread without acquiring lock --- client/CServerHandler.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 18e9ee444..667684b2b 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -88,6 +88,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);