From 219a282916ee7f40fbc1403a5e25457e3eb0aaed Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 3 Nov 2022 21:38:49 +0400 Subject: [PATCH 01/17] First step --- AI/BattleAI/BattleAI.h | 2 +- AI/Nullkiller/AIGateway.cpp | 4 ++-- AI/Nullkiller/AIGateway.h | 2 +- AI/StupidAI/StupidAI.cpp | 2 +- AI/StupidAI/StupidAI.h | 2 +- AI/VCAI/VCAI.cpp | 4 ++-- AI/VCAI/VCAI.h | 2 +- client/CPlayerInterface.cpp | 15 ++++++++++---- client/CPlayerInterface.h | 2 +- client/Client.cpp | 2 +- client/NetPacksClient.cpp | 2 +- client/battle/CBattleInterface.cpp | 24 +++++++++++++++------- client/battle/CBattleInterface.h | 4 ++-- client/battle/CBattleInterfaceClasses.cpp | 25 ++++++++++++++++++++++- client/battle/CBattleInterfaceClasses.h | 5 ++++- lib/CGameInterface.cpp | 4 ++-- lib/CGameInterface.h | 2 +- lib/IGameEventsReceiver.h | 2 +- lib/NetPacks.h | 4 +++- server/CGameHandler.cpp | 4 ++++ server/CQuery.cpp | 10 +++++++++ server/CQuery.h | 6 ++++++ 22 files changed, 97 insertions(+), 32 deletions(-) diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 82cecacc8..552340543 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -81,7 +81,7 @@ public: //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 //void battleStacksAttacked(const std::vector & bsa) override; //called when stack receives damage (after battleAttack()) - //void battleEnd(const BattleResult *br) override; + //void battleEnd(const BattleResult *br, QueryID queryID) override; //void battleResultsApplied() override; //called when all effects of last battle are applied //void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; //void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 6a6dfd299..f04202705 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1067,7 +1067,7 @@ void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * arm CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); } -void AIGateway::battleEnd(const BattleResult * br) +void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) { NET_EVENT_HANDLER; assert(status.getBattle() == ONGOING_BATTLE); @@ -1075,7 +1075,7 @@ void AIGateway::battleEnd(const BattleResult * br) bool won = br->winner == myCb->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); - CAdventureAI::battleEnd(br); + CAdventureAI::battleEnd(br, queryID); } void AIGateway::waitTillFree() diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index 54854d702..af42cf684 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -169,7 +169,7 @@ public: boost::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 battleEnd(const BattleResult * br) override; + void battleEnd(const BattleResult * br, QueryID queryID) override; void makeTurn(); diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 8a2bf194e..d9ccf0654 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -182,7 +182,7 @@ void CStupidAI::battleStacksAttacked(const std::vector & bs print("battleStacksAttacked called"); } -void CStupidAI::battleEnd(const BattleResult *br) +void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID) { print("battleEnd called"); } diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 07a2ebcac..8cb33369c 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -32,7 +32,7 @@ public: void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack void battleStacksAttacked(const std::vector & bsa) override; //called when stack receives damage (after battleAttack()) - void battleEnd(const BattleResult *br) override; + void battleEnd(const BattleResult *br, QueryID queryID) override; //void battleResultsApplied() override; //called when all effects of last battle are applied void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 7f42b8d70..851c7b70f 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1583,7 +1583,7 @@ void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, i CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); } -void VCAI::battleEnd(const BattleResult * br) +void VCAI::battleEnd(const BattleResult * br, QueryID queryID) { NET_EVENT_HANDLER; assert(status.getBattle() == ONGOING_BATTLE); @@ -1591,7 +1591,7 @@ void VCAI::battleEnd(const BattleResult * br) bool won = br->winner == myCb->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); - CAdventureAI::battleEnd(br); + CAdventureAI::battleEnd(br, queryID); } void VCAI::waitTillFree() diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index acb6e5f5d..a859e14bd 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -201,7 +201,7 @@ public: void showWorldViewEx(const std::vector & objectPositions) override; void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; - void battleEnd(const BattleResult * br) override; + void battleEnd(const BattleResult * br, QueryID queryID) override; void makeTurn(); void mainLoop(); diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index af40aa43f..f461ce914 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -707,7 +707,9 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) { EVENT_HANDLER_CALLED_BY_CLIENT; - if (settings["adventure"]["quickCombat"].Bool()) + //quick combat with neutral creatures only + auto * army2_object = dynamic_cast(army2); + if(army2_object && army2_object->getOwner() == PlayerColor::UNFLAGGABLE && settings["adventure"]["quickCombat"].Bool()) { autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI->init(env, cb); @@ -922,7 +924,7 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i return ret; } -void CPlayerInterface::battleEnd(const BattleResult *br) +void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; if(isAutoFightOn || autofightingAI) @@ -933,7 +935,12 @@ void CPlayerInterface::battleEnd(const BattleResult *br) if(!battleInt) { - GH.pushIntT(*br, *this); + auto wnd = std::make_shared(*br, *this, true); + wnd->resultCallback = [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }; + GH.pushInt(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. waitWhileDialog(); @@ -944,7 +951,7 @@ void CPlayerInterface::battleEnd(const BattleResult *br) BATTLE_EVENT_POSSIBLE_RETURN; - battleInt->battleFinished(*br); + battleInt->battleFinished(*br, queryID); adventureInt->quickCombatUnlock(); } diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index f7907594c..9878b9ff4 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -189,7 +189,7 @@ public: void actionStarted(const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack void battleAttack(const BattleAttack *ba) override; //stack performs attack - void battleEnd(const BattleResult *br) override; //end of battle + void battleEnd(const BattleResult *br, QueryID queryID) override; //end of battle void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; used for HP regen handling void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn void battleLogMessage(const std::vector & lines) override; diff --git a/client/Client.cpp b/client/Client.cpp index 5ee3c6cb0..a84849537 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -583,7 +583,7 @@ void CClient::battleStarted(const BattleInfo * info) auto & leftSide = info->sides[0], & rightSide = info->sides[1]; //If quick combat is not, do not prepare interfaces for battleint - if(!settings["adventure"]["quickCombat"].Bool()) + if(rightSide.color != PlayerColor::NEUTRAL || !settings["adventure"]["quickCombat"].Bool()) { if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) att = std::dynamic_pointer_cast(playerint[leftSide.color]); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index d44434b18..d9826a3da 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -708,7 +708,7 @@ void BattleUpdateGateState::applyFirstCl(CClient * cl) void BattleResult::applyFirstCl(CClient *cl) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, this); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, this, queryID); cl->battleFinished(); } diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index 6606e6dd8..ba7c9cb7c 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -859,11 +859,14 @@ void CBattleInterface::reallySurrender() void CBattleInterface::bAutofightf() { - if (spellDestSelectMode) //we are casting a spell + //if(bresult) //battle is already finished + //return; + + if(spellDestSelectMode) //we are casting a spell return; //Stop auto-fight mode - if (curInt->isAutoFightOn) + if(curInt->isAutoFightOn) { assert(curInt->autofightingAI); curInt->isAutoFightOn = false; @@ -1129,7 +1132,8 @@ void CBattleInterface::newRoundFirst( int round ) void CBattleInterface::newRound(int number) { - console->addText(CGI->generaltexth->allTexts[412]); + if(console) + console->addText(CGI->generaltexth->allTexts[412]); } void CBattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional) @@ -1254,7 +1258,7 @@ void CBattleInterface::stackIsCatapulting(const CatapultAttack & ca) } } -void CBattleInterface::battleFinished(const BattleResult& br) +void CBattleInterface::battleFinished(const BattleResult& br, QueryID queryID) { bresult = &br; { @@ -1262,19 +1266,25 @@ void CBattleInterface::battleFinished(const BattleResult& br) animsAreDisplayed.waitUntil(false); } setActiveStack(nullptr); - displayBattleFinished(); + displayBattleFinished(queryID); } -void CBattleInterface::displayBattleFinished() +void CBattleInterface::displayBattleFinished(QueryID queryID) { CCS->curh->changeGraphic(ECursor::ADVENTURE,0); if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool()) { + curInt->cb->selectionMade(0, queryID); close(); return; } - GH.pushInt(std::make_shared(*bresult, *(this->curInt))); + auto wnd = std::make_shared(*bresult, *(this->curInt)); + wnd->resultCallback = [=](ui32 selection) + { + curInt->cb->selectionMade(selection, queryID); + }; + GH.pushInt(wnd); curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 CPlayerInterface::battleInt = nullptr; } diff --git a/client/battle/CBattleInterface.h b/client/battle/CBattleInterface.h index 0fdd232fe..186a3283c 100644 --- a/client/battle/CBattleInterface.h +++ b/client/battle/CBattleInterface.h @@ -353,8 +353,8 @@ public: void newRound(int number); //caled when round is ended; number is the number of round void hexLclicked(int whichOne); //hex only call-in void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls - void battleFinished(const BattleResult& br); //called when battle is finished - battleresult window should be printed - void displayBattleFinished(); //displays battle result + void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed + void displayBattleFinished(QueryID queryID); //displays battle result void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook diff --git a/client/battle/CBattleInterfaceClasses.cpp b/client/battle/CBattleInterfaceClasses.cpp index f9902989f..b0b0b47df 100644 --- a/client/battle/CBattleInterfaceClasses.cpp +++ b/client/battle/CBattleInterfaceClasses.cpp @@ -394,7 +394,7 @@ void CBattleOptionsWindow::bExitf() close(); } -CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner) +CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay) : owner(_owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -405,6 +405,12 @@ CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterfa exit = std::make_shared(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, SDLK_RETURN); exit->setBorderColor(Colors::METALLIC_GOLD); + + if(allowReplay) + { + repeat = std::make_shared(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, SDLK_ESCAPE); + repeat->setBorderColor(Colors::METALLIC_GOLD); + } if(br.winner == 0) //attacker won { @@ -566,6 +572,7 @@ void CBattleResultWindow::show(SDL_Surface * to) void CBattleResultWindow::bExitf() { + resultCallback(0); CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon close(); @@ -579,6 +586,22 @@ void CBattleResultWindow::bExitf() CCS->videoh->close(); } +void CBattleResultWindow::bRepeatf() +{ + resultCallback(1); + CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon + + close(); + + if(dynamic_cast(GH.topInt().get())) + GH.popInts(1); //pop battle interface if present + + //Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle, + //so we can be sure that there is no dialogs left on GUI stack. + intTmp.showingDialog->setn(false); + CCS->videoh->close(); +} + Point CClickableHex::getXYUnitAnim(BattleHex hexNum, const CStack * stack, CBattleInterface * cbi) { assert(cbi); diff --git a/client/battle/CBattleInterfaceClasses.h b/client/battle/CBattleInterfaceClasses.h index c0b657d27..531b83cf2 100644 --- a/client/battle/CBattleInterfaceClasses.h +++ b/client/battle/CBattleInterfaceClasses.h @@ -118,14 +118,17 @@ private: std::shared_ptr background; std::vector> labels; std::shared_ptr exit; + std::shared_ptr repeat; std::vector> icons; std::shared_ptr description; CPlayerInterface & owner; public: - CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner); + CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false); ~CBattleResultWindow(); void bExitf(); //exit button callback + void bRepeatf(); //repeat button callback + std::function resultCallback; //callback receiving which button was pressed void activate() override; void show(SDL_Surface * to = 0) override; diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index f47fa1bdf..34b78b0eb 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -219,9 +219,9 @@ void CAdventureAI::battleSpellCast(const BattleSpellCast * sc) battleAI->battleSpellCast(sc); } -void CAdventureAI::battleEnd(const BattleResult * br) +void CAdventureAI::battleEnd(const BattleResult * br, QueryID queryID) { - battleAI->battleEnd(br); + battleAI->battleEnd(br, queryID); battleAI.reset(); } diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 99c166c3f..4d1d2c003 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -163,7 +163,7 @@ public: virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance) override; virtual void battleAttack(const BattleAttack *ba) override; virtual void battleSpellCast(const BattleSpellCast *sc) override; - virtual void battleEnd(const BattleResult *br) override; + virtual void battleEnd(const BattleResult *br, QueryID queryID) override; virtual void battleUnitsChanged(const std::vector & units, const std::vector & customEffects) override; virtual void saveGame(BinarySerializer & h, const int version) override; diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 632489c76..c55916be2 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -60,7 +60,7 @@ public: virtual void actionStarted(const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero virtual void battleAttack(const BattleAttack *ba){}; //called when stack is performing attack virtual void battleStacksAttacked(const std::vector & bsa){}; //called when stack receives damage (after battleAttack()) - virtual void battleEnd(const BattleResult *br){}; + virtual void battleEnd(const BattleResult *br, QueryID queryID){}; virtual void battleNewRoundFirst(int round){}; //called at the beginning of each turn before changes are applied; virtual void battleNewRound(int round){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn virtual void battleLogMessage(const std::vector & lines){}; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 171ebdace..19b8f4c7a 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1434,7 +1434,7 @@ struct BattleSetActiveStack : public CPackForClient } }; -struct BattleResult : public CPackForClient +struct BattleResult : public Query { enum EResult {NORMAL = 0, ESCAPE = 1, SURRENDER = 2}; @@ -1447,6 +1447,7 @@ struct BattleResult : public CPackForClient void applyFirstCl(CClient *cl); void applyGs(CGameState *gs); + EResult result; ui8 winner; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] std::map casualties[2]; //first => casualties of attackers - map crid => number @@ -1455,6 +1456,7 @@ struct BattleResult : public CPackForClient template void serialize(Handler &h, const int version) { + h & queryID; h & result; h & winner; h & casualties[0]; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index a4f8dedad..00d82a9ab 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -818,6 +818,10 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con } } } + + auto battleDialogQuery = std::make_shared(this); + battleResult.data->queryID = battleDialogQuery->queryID; + queries.addQuery(battleDialogQuery); sendAndApply(battleResult.data); //after this point casualties objects are destroyed if (arts.size()) //display loot diff --git a/server/CQuery.cpp b/server/CQuery.cpp index 4f8270a6f..d907c03ca 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -380,6 +380,16 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const return CDialogQuery::blocksPack(pack); } +void CBattleDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + if(*answer == 1) + { + int a = 0; + ++a; + } +} + void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { assert(answer); diff --git a/server/CQuery.h b/server/CQuery.h index aa5446f3c..03705a622 100644 --- a/server/CQuery.h +++ b/server/CQuery.h @@ -144,6 +144,12 @@ public: virtual bool blocksPack(const CPack *pack) const override; }; +class CBattleDialogQuery : public CDialogQuery +{ +public: + virtual void onRemoval(PlayerColor color) override; +}; + //yes/no and component selection dialogs class CBlockingDialogQuery : public CDialogQuery { From 48925a50f29bb621e17156e9495c10b15bf93748 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 3 Nov 2022 23:48:55 +0400 Subject: [PATCH 02/17] Development --- lib/battle/BattleInfo.cpp | 1 + lib/battle/BattleInfo.h | 1 + server/CGameHandler.cpp | 2 +- server/CQuery.cpp | 12 ++++++++++-- server/CQuery.h | 4 ++++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index ea5e7e776..abdddc8e1 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -206,6 +206,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const curB->battlefieldType = battlefieldType; curB->round = -2; curB->activeStack = -1; + curB->creatureBank = creatureBank; if(town) { diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 76d3a9db2..5647e68b9 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -34,6 +34,7 @@ public: si32 round, activeStack; 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 std::vector stacks; std::vector > obstacles; SiegeInfo si; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 00d82a9ab..a3a7c6098 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -819,7 +819,7 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con } } - auto battleDialogQuery = std::make_shared(this); + auto battleDialogQuery = std::make_shared(this, battleQuery->bi); battleResult.data->queryID = battleDialogQuery->queryID; queries.addQuery(battleDialogQuery); sendAndApply(battleResult.data); //after this point casualties objects are destroyed diff --git a/server/CQuery.cpp b/server/CQuery.cpp index d907c03ca..0a1395a26 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -379,14 +379,22 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const return CDialogQuery::blocksPack(pack); } + +CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): + CDialogQuery(owner) +{ + bi = Bi; + + for(auto & side : bi->sides) + addPlayer(side.color); +} void CBattleDialogQuery::onRemoval(PlayerColor color) { assert(answer); if(*answer == 1) { - int a = 0; - ++a; + gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); } } diff --git a/server/CQuery.h b/server/CQuery.h index 03705a622..969e53ddc 100644 --- a/server/CQuery.h +++ b/server/CQuery.h @@ -147,6 +147,10 @@ public: class CBattleDialogQuery : public CDialogQuery { public: + CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi); + + const BattleInfo * bi; + virtual void onRemoval(PlayerColor color) override; }; From 0c4e50b63cb379be8be6001664d52d21b5858072 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 5 Nov 2022 03:23:31 +0400 Subject: [PATCH 03/17] Initial prototype works --- client/CPlayerInterface.cpp | 5 +- client/CPlayerInterface.h | 1 + client/Client.cpp | 39 ++++++++-------- server/CGameHandler.cpp | 92 ++++++++++++++++++++++--------------- server/CGameHandler.h | 7 ++- server/CQuery.cpp | 4 ++ 6 files changed, 89 insertions(+), 59 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index f461ce914..05b1f1c22 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -707,9 +707,12 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) { EVENT_HANDLER_CALLED_BY_CLIENT; + bool replay = (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(army2_object && army2_object->getOwner() == PlayerColor::UNFLAGGABLE && settings["adventure"]["quickCombat"].Bool()) + if(!replay && army2_object && army2_object->getOwner() == PlayerColor::UNFLAGGABLE && settings["adventure"]["quickCombat"].Bool()) { autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI->init(env, cb); diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 9878b9ff4..ffac2611e 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -69,6 +69,7 @@ namespace boost class CPlayerInterface : public CGameInterface, public IUpdateable { const CArmedInstance * currentSelection; + std::pair lastBattleArmies; public: std::shared_ptr env; diff --git a/client/Client.cpp b/client/Client.cpp index a84849537..dff6faac7 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -581,15 +581,30 @@ void CClient::battleStarted(const BattleInfo * info) std::shared_ptr att, def; auto & leftSide = info->sides[0], & rightSide = info->sides[1]; + + 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); + }; + + callBattleStart(leftSide.color, 0); + callBattleStart(rightSide.color, 1); + callBattleStart(PlayerColor::UNFLAGGABLE, 1); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + callBattleStart(PlayerColor::SPECTATOR, 1); + + if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) + att = std::dynamic_pointer_cast(playerint[leftSide.color]); + + if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) + def = std::dynamic_pointer_cast(playerint[rightSide.color]); //If quick combat is not, do not prepare interfaces for battleint - if(rightSide.color != PlayerColor::NEUTRAL || !settings["adventure"]["quickCombat"].Bool()) + if(att && att->isAutoFightOn) { - if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) - att = std::dynamic_pointer_cast(playerint[leftSide.color]); - - if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) - def = std::dynamic_pointer_cast(playerint[rightSide.color]); + att.reset(); + def.reset(); } if(!settings["session"]["headless"].Bool()) @@ -610,18 +625,6 @@ 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); - }; - - callBattleStart(leftSide.color, 0); - callBattleStart(rightSide.color, 1); - callBattleStart(PlayerColor::UNFLAGGABLE, 1); - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - callBattleStart(PlayerColor::SPECTATOR, 1); - if(info->tacticDistance && vstd::contains(battleints, info->sides[info->tacticsSide].color)) { boost::thread(&CClient::commenceTacticPhaseForInt, this, battleints[info->sides[info->tacticsSide].color]); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index a3a7c6098..02276981f 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -703,11 +703,7 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con battleResult.data->exp[0] = heroAttacker->calculateXp(battleResult.data->exp[0]);//scholar skill if(heroDefender) battleResult.data->exp[1] = heroDefender->calculateXp(battleResult.data->exp[1]); - - const CArmedInstance *bEndArmy1 = gs->curB->sides.at(0).armyObject; - const CArmedInstance *bEndArmy2 = gs->curB->sides.at(1).armyObject; - const BattleResult::EResult result = battleResult.get()->result; - + auto findBattleQuery = [this]() -> std::shared_ptr { for (auto &q : queries.allQueries()) @@ -732,8 +728,39 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con //Check how many battle queries were created (number of players blocked by battle) const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; finishingBattle = make_unique(battleQuery, queriedPlayers); + + auto battleDialogQuery = std::make_shared(this, gs->curB); + battleResult.data->queryID = battleDialogQuery->queryID; + queries.addQuery(battleDialogQuery); + sendAndApply(battleResult.data); //after this point casualties objects are destroyed +} - CasualtiesAfterBattle cab1(bEndArmy1, gs->curB), cab2(bEndArmy2, gs->curB); //calculate casualties before deleting battle +void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) +{ + auto findBattleQuery = [this, battleInfo]() -> std::shared_ptr + { + for (auto &q : queries.allQueries()) + { + if (auto bq = std::dynamic_pointer_cast(q)) + if (bq->bi == battleInfo) + return bq; + } + return std::shared_ptr(); + }; + + auto battleQuery = findBattleQuery(); + if (!battleQuery) + { + logGlobal->error("Cannot find battle query!"); + } + if (battleQuery != queries.topQuery(battleInfo->sides[0].color)) + complain("Player " + boost::lexical_cast(battleInfo->sides[0].color) + " although in battle has no battle query at the top!"); + + 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 ChangeSpells cs; //for Eagle Eye if (finishingBattle->winnerHero) @@ -741,7 +768,7 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_VAL2, SecondarySkill::EAGLE_EYE)) { double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::EAGLE_EYE); - for(auto & spellId : gs->curB->sides.at(!battleResult.data->winner).usedSpellsHistory) + for(auto & spellId : battleInfo->sides.at(!battleResult.data->winner).usedSpellsHistory) { auto spell = spellId.toSpell(VLC->spells()); if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && getRandomGenerator().nextInt(99) < eagleEyeChance) @@ -770,8 +797,8 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); const CArtifactInstance * art = ma.src.getArt(); if (art && !art->artType->isBig() && - art->artType->id != ArtifactID::SPELLBOOK) - // don't move war machines or locked arts (spellbook) + art->artType->id != ArtifactID::SPELLBOOK) + // don't move war machines or locked arts (spellbook) { sendMoveArtifact(art, &ma); } @@ -781,7 +808,7 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con //we assume that no big artifacts can be found MoveArtifact ma; ma.src = ArtifactLocation(finishingBattle->loserHero, - ArtifactPosition(GameConstants::BACKPACK_START)); //backpack automatically shifts arts to beginning + ArtifactPosition(GameConstants::BACKPACK_START)); //backpack automatically shifts arts to beginning const CArtifactInstance * art = ma.src.getArt(); if (art->artType->id != ArtifactID::GRAIL) //grail may not be won { @@ -803,7 +830,7 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con } } } - for (auto armySlot : gs->curB->battleGetArmyObject(!battleResult.data->winner)->stacks) + for (auto armySlot : battleInfo->sides.at(!battleResult.data->winner).armyObject->stacks) { auto artifactsWorn = armySlot.second->artifactsWorn; for (auto artSlot : artifactsWorn) @@ -819,11 +846,6 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con } } - auto battleDialogQuery = std::make_shared(this, battleQuery->bi); - battleResult.data->queryID = battleDialogQuery->queryID; - queries.addQuery(battleDialogQuery); - sendAndApply(battleResult.data); //after this point casualties objects are destroyed - if (arts.size()) //display loot { InfoWindow iw; @@ -884,33 +906,25 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con } cab1.updateArmy(this); cab2.updateArmy(this); //take casualties after battle is deleted - - if(battleResult.data->winner != BattleSide::ATTACKER && heroAttacker) //remove beaten Attacker + + if(finishingBattle->loserHero) //remove beaten hero { - RemoveObject ro(heroAttacker->id); + RemoveObject ro(finishingBattle->loserHero->id); sendAndApply(&ro); } - - if(battleResult.data->winner != BattleSide::DEFENDER && heroDefender) //remove beaten Defender + + if(battleResult.data->winner == BattleSide::DEFENDER + && finishingBattle->winnerHero + && finishingBattle->winnerHero->visitedTown + && !finishingBattle->winnerHero->inTownGarrison + && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) { - RemoveObject ro(heroDefender->id); - sendAndApply(&ro); - } - - if(battleResult.data->winner == BattleSide::DEFENDER - && heroDefender - && heroDefender->visitedTown - && !heroDefender->inTownGarrison - && heroDefender->visitedTown->garrisonHero == heroDefender) - { - swapGarrisonOnSiege(heroDefender->visitedTown->id); //return defending visitor from garrison to its rightful place + swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place } //give exp - if(battleResult.data->exp[0] && heroAttacker && battleResult.get()->winner == BattleSide::ATTACKER) - changePrimSkill(heroAttacker, PrimarySkill::EXPERIENCE, battleResult.data->exp[0]); - else if(battleResult.data->exp[1] && heroDefender && battleResult.get()->winner == BattleSide::DEFENDER) - changePrimSkill(heroDefender, PrimarySkill::EXPERIENCE, battleResult.data->exp[1]); - + if(battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) + changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); + queries.popIfTop(battleQuery); //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query (above) @@ -7259,7 +7273,7 @@ void CGameHandler::showInfoDialog(const std::string & msg, PlayerColor player) showInfoDialog(&iw); } -CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, BattleInfo *bat): +CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, const BattleInfo * bat): army(_army) { heroWithDeadCommander = ObjectInstanceID(); @@ -7411,12 +7425,14 @@ CGameHandler::FinishingBattleHelper::FinishingBattleHelper(std::shared_ptrstartBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); } + else + { + gh->endBattleConfirm(bi); + } } void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const From e9cf3ede2a3710f041311fc02cc6b8c559d0105d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 5 Nov 2022 06:34:38 +0400 Subject: [PATCH 04/17] Try to handle queries by ai --- AI/Nullkiller/AIGateway.cpp | 3 ++ AI/VCAI/VCAI.cpp | 3 ++ lib/NetPacks.h | 20 ++++++++++++ lib/NetPacksLib.cpp | 29 +++++++++++------- lib/registerTypes/RegisterTypes.h | 1 + server/CGameHandler.cpp | 51 +++++++++++++------------------ 6 files changed, 66 insertions(+), 41 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index f04202705..cf52a8501 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1075,6 +1075,9 @@ 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"); + answerQuery(queryID, 0); + status.removeQuery(queryID); //do not wait for answer CAdventureAI::battleEnd(br, queryID); } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 851c7b70f..ececd8112 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1591,6 +1591,9 @@ 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"); + answerQuery(queryID, 0); + status.removeQuery(queryID); //do not wait for answer CAdventureAI::battleEnd(br, queryID); } diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 19b8f4c7a..e21ed2fd2 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1434,6 +1434,26 @@ struct BattleSetActiveStack : public CPackForClient } }; +struct BattleResultAccepted : public CPackForClient +{ + void applyGs(CGameState * gs); + + CGHeroInstance * hero1 = nullptr; + CGHeroInstance * hero2 = nullptr; + CArmedInstance * army1 = nullptr; + CArmedInstance * army2 = nullptr; + TExpType exp[2]; + + template void serialize(Handler &h, const int version) + { + h & hero1; + h & hero2; + h & army1; + h & army2; + h & exp; + } +}; + struct BattleResult : public Query { enum EResult {NORMAL = 0, ESCAPE = 1, SURRENDER = 2}; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 6339d77ee..0f8a091c8 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1390,10 +1390,17 @@ void BattleResult::applyGs(CGameState *gs) for (auto & elem : gs->curB->stacks) delete elem; + for(int i = 0; i < 2; i++) + gs->curB->battleGetArmyObject(i)->battle = nullptr; - for(int i = 0; i < 2; ++i) + gs->curB.dellNull(); +} + +void BattleResultAccepted::applyGs(CGameState * gs) +{ + for(auto * h : {hero1, hero2}) { - if(auto h = gs->curB->battleGetFightingHero(i)) + if(h) { h->removeBonusesRecursive(Bonus::OneBattle); //remove any "until next battle" bonuses if (h->commander && h->commander->alive) @@ -1405,20 +1412,20 @@ void BattleResult::applyGs(CGameState *gs) } } } - + if(VLC->modh->modules.STACK_EXP) { for(int i = 0; i < 2; i++) + { if(exp[i]) - gs->curB->battleGetArmyObject(i)->giveStackExp(exp[i]); - + { + if(auto * army = (i == 0 ? army1 : army2)) + army->giveStackExp(exp[i]); + } + } + CBonusSystemNode::treeHasChanged(); - } - - for(int i = 0; i < 2; i++) - gs->curB->battleGetArmyObject(i)->battle = nullptr; - - gs->curB.dellNull(); + } } DLL_LINKAGE void BattleLogMessage::applyGs(CGameState *gs) diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 519643e33..3b8904ee1 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -280,6 +280,7 @@ void registerTypesClientPacks2(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 02276981f..68fab7792 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -703,25 +703,14 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con battleResult.data->exp[0] = heroAttacker->calculateXp(battleResult.data->exp[0]);//scholar skill if(heroDefender) battleResult.data->exp[1] = heroDefender->calculateXp(battleResult.data->exp[1]); - - auto findBattleQuery = [this]() -> std::shared_ptr - { - for (auto &q : queries.allQueries()) - { - if (auto bq = std::dynamic_pointer_cast(q)) - if (bq->bi == gs->curB) - return bq; - } - return std::shared_ptr(); - }; - auto battleQuery = findBattleQuery(); + auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); if (!battleQuery) { logGlobal->error("Cannot find battle query!"); + complain("Player " + boost::lexical_cast(gs->curB->sides[0].color) + " has no battle query at the top!"); + return; } - if (battleQuery != queries.topQuery(gs->curB->sides[0].color)) - complain("Player " + boost::lexical_cast(gs->curB->sides[0].color) + " although in battle has no battle query at the top!"); battleQuery->result = boost::make_optional(*battleResult.data); @@ -737,24 +726,13 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) { - auto findBattleQuery = [this, battleInfo]() -> std::shared_ptr - { - for (auto &q : queries.allQueries()) - { - if (auto bq = std::dynamic_pointer_cast(q)) - if (bq->bi == battleInfo) - return bq; - } - return std::shared_ptr(); - }; - - auto battleQuery = findBattleQuery(); - if (!battleQuery) + auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(battleInfo->sides.at(0).color)); + if(!battleQuery) { logGlobal->error("Cannot find battle query!"); + complain("Player " + boost::lexical_cast(battleInfo->sides.at(0).color) + " has no battle query at the top!"); + return; } - if (battleQuery != queries.topQuery(battleInfo->sides[0].color)) - complain("Player " + boost::lexical_cast(battleInfo->sides[0].color) + " although in battle has no battle query at the top!"); const CArmedInstance *bEndArmy1 = battleInfo->sides.at(0).armyObject; const CArmedInstance *bEndArmy2 = battleInfo->sides.at(1).armyObject; @@ -926,6 +904,15 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); queries.popIfTop(battleQuery); + + BattleResultAccepted raccepted; + raccepted.army1 = const_cast(bEndArmy1); + raccepted.army2 = const_cast(bEndArmy2); + raccepted.hero1 = const_cast(battleInfo->sides.at(0).hero); + raccepted.hero2 = const_cast(battleInfo->sides.at(1).hero); + raccepted.exp[0] = battleResult.data->exp[0]; + raccepted.exp[1] = battleResult.data->exp[1]; + sendAndApply(&raccepted); //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query (above) } @@ -934,7 +921,9 @@ void CGameHandler::battleAfterLevelUp(const BattleResult &result) { LOG_TRACE(logGlobal); - + if(!finishingBattle) + return; + finishingBattle->remainingBattleQueriesCount--; logGlobal->trace("Decremented queries count to %d", finishingBattle->remainingBattleQueriesCount); @@ -1016,6 +1005,8 @@ void CGameHandler::battleAfterLevelUp(const BattleResult &result) sendAndApply(&sah); } } + + finishingBattle.reset(); } void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) From 7883fee55a2806191649c81382b93d4f20dc91f3 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 5 Nov 2022 21:59:56 +0400 Subject: [PATCH 05/17] Fix AI hanging during combat result dialog --- AI/Nullkiller/AIGateway.cpp | 7 +++++-- AI/VCAI/VCAI.cpp | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index cf52a8501..35171e66a 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1076,8 +1076,11 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); status.addQuery(queryID, "Combat result dialog"); - answerQuery(queryID, 0); - status.removeQuery(queryID); //do not wait for answer + const int confirmAction = 0; + requestActionASAP([=]() + { + answerQuery(queryID, confirmAction); + }); CAdventureAI::battleEnd(br, queryID); } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index ececd8112..9ab9251ae 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1592,8 +1592,11 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID) logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); status.addQuery(queryID, "Combat result dialog"); - answerQuery(queryID, 0); - status.removeQuery(queryID); //do not wait for answer + const int confirmAction = 0; + requestActionASAP([=]() + { + answerQuery(queryID, confirmAction); + }); CAdventureAI::battleEnd(br, queryID); } From dcda3595672e1587161c31079916872a80f8fcb1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 5 Nov 2022 22:51:02 +0400 Subject: [PATCH 06/17] Enable quick combat by default --- config/schemas/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 2975c852f..7ad183191 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -144,7 +144,7 @@ }, "quickCombat" : { "type" : "boolean", - "default" : false + "default" : true } } }, From 4ccad9178eac97011ef0a291cbf9b227df7ea324 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 6 Nov 2022 21:46:56 +0400 Subject: [PATCH 07/17] Fix potential BattleInfo destruction before usage --- lib/NetPacks.h | 2 -- lib/NetPacksLib.cpp | 15 +++------------ lib/battle/BattleInfo.cpp | 10 +++++++++- server/CGameHandler.cpp | 3 +++ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/NetPacks.h b/lib/NetPacks.h index e21ed2fd2..a4a4051c7 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1465,9 +1465,7 @@ struct BattleResult : public Query exp[1] = 0; }; void applyFirstCl(CClient *cl); - void applyGs(CGameState *gs); - EResult result; ui8 winner; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] std::map casualties[2]; //first => casualties of attackers - map crid => number diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 0f8a091c8..4f5c99887 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1385,17 +1385,6 @@ DLL_LINKAGE void BattleUpdateGateState::applyGs(CGameState *gs) gs->curB->si.gateState = state; } -void BattleResult::applyGs(CGameState *gs) -{ - for (auto & elem : gs->curB->stacks) - delete elem; - - for(int i = 0; i < 2; i++) - gs->curB->battleGetArmyObject(i)->battle = nullptr; - - gs->curB.dellNull(); -} - void BattleResultAccepted::applyGs(CGameState * gs) { for(auto * h : {hero1, hero2}) @@ -1425,7 +1414,9 @@ void BattleResultAccepted::applyGs(CGameState * gs) } CBonusSystemNode::treeHasChanged(); - } + } + + gs->curB.dellNull(); } DLL_LINKAGE void BattleLogMessage::applyGs(CGameState *gs) diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index abdddc8e1..91766ea5a 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -539,7 +539,15 @@ BattleInfo::BattleInfo(): setNodeType(BATTLE); } -BattleInfo::~BattleInfo() = default; +BattleInfo::~BattleInfo() +{ + for (auto & elem : stacks) + delete elem; + + for(int i = 0; i < 2; i++) + if(auto * _armyObj = battleGetArmyObject(i)) + _armyObj->battle = nullptr; +} int32_t BattleInfo::getActiveStackID() const { diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 68fab7792..6b4740fe7 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2707,6 +2707,9 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) //use hero=nullptr for no hero { + if(gs->curB) + gs->curB.dellNull(); + engageIntoBattle(army1->tempOwner); engageIntoBattle(army2->tempOwner); From b253b19dc3c52d16dd3355904291b93fa5fbcde2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 16 Nov 2022 02:50:47 +0400 Subject: [PATCH 08/17] Support draw scenario (doesnt work properly) --- client/CPlayerInterface.cpp | 10 +++++++--- client/Client.cpp | 2 +- client/battle/CBattleInterfaceClasses.cpp | 22 ++++++++-------------- client/battle/CBattleInterfaceClasses.h | 2 ++ config/schemas/settings.json | 10 ++++++++-- server/CGameHandler.cpp | 16 +++++++++++----- server/CGameHandler.h | 2 ++ 7 files changed, 39 insertions(+), 25 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 09b83c7dc..5809c01c4 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -707,12 +707,15 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) { EVENT_HANDLER_CALLED_BY_CLIENT; - bool replay = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2); + bool replay = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2); //will be true if player already refused auto-battle result lastBattleArmies.first = army1; lastBattleArmies.second = army2; //quick combat with neutral creatures only auto * army2_object = dynamic_cast(army2); - if(!replay && army2_object && army2_object->getOwner() == PlayerColor::UNFLAGGABLE && settings["adventure"]["quickCombat"].Bool()) + if((!replay && army2_object + && army2_object->getOwner() == PlayerColor::UNFLAGGABLE + && settings["adventure"]["quickCombat"].Bool()) + || settings["adventure"]["alwaysSkipBattle"].Bool()) { autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI->init(env, cb); @@ -938,7 +941,8 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) if(!battleInt) { - auto wnd = std::make_shared(*br, *this, true); + bool replay = !settings["adventure"]["alwaysSkipCombat"].Bool(); //do not allow manual replay + auto wnd = std::make_shared(*br, *this, replay); wnd->resultCallback = [=](ui32 selection) { cb->selectionMade(selection, queryID); diff --git a/client/Client.cpp b/client/Client.cpp index dff6faac7..49cd93d7f 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -600,7 +600,7 @@ void CClient::battleStarted(const BattleInfo * info) if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) def = std::dynamic_pointer_cast(playerint[rightSide.color]); - //If quick combat is not, do not prepare interfaces for battleint + //Remove player interfaces for auto battle (quickCombat option) if(att && att->isAutoFightOn) { att.reset(); diff --git a/client/battle/CBattleInterfaceClasses.cpp b/client/battle/CBattleInterfaceClasses.cpp index 81467d752..1e10c9a12 100644 --- a/client/battle/CBattleInterfaceClasses.cpp +++ b/client/battle/CBattleInterfaceClasses.cpp @@ -570,9 +570,9 @@ void CBattleResultWindow::show(SDL_Surface * to) CCS->videoh->update(pos.x + 107, pos.y + 70, screen, true, false); } -void CBattleResultWindow::bExitf() +void CBattleResultWindow::buttonPressed(int button) { - resultCallback(0); + resultCallback(button); CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon close(); @@ -586,20 +586,14 @@ void CBattleResultWindow::bExitf() CCS->videoh->close(); } +void CBattleResultWindow::bExitf() +{ + buttonPressed(0); +} + void CBattleResultWindow::bRepeatf() { - resultCallback(1); - CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon - - close(); - - if(dynamic_cast(GH.topInt().get())) - GH.popInts(1); //pop battle interface if present - - //Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle, - //so we can be sure that there is no dialogs left on GUI stack. - intTmp.showingDialog->setn(false); - CCS->videoh->close(); + buttonPressed(1); } Point CClickableHex::getXYUnitAnim(BattleHex hexNum, const CStack * stack, CBattleInterface * cbi) diff --git a/client/battle/CBattleInterfaceClasses.h b/client/battle/CBattleInterfaceClasses.h index 531b83cf2..9b853f028 100644 --- a/client/battle/CBattleInterfaceClasses.h +++ b/client/battle/CBattleInterfaceClasses.h @@ -122,6 +122,8 @@ private: std::vector> icons; std::shared_ptr description; CPlayerInterface & owner; + + void buttonPressed(int button); //internal function for button callbacks public: CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false); ~CBattleResultWindow(); diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 7ad183191..e64afd97e 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -124,7 +124,7 @@ "type" : "object", "additionalProperties" : false, "default": {}, - "required" : [ "heroSpeed", "enemySpeed", "scrollSpeed", "heroReminder", "quickCombat" ], + "required" : [ "heroSpeed", "enemySpeed", "scrollSpeed", "heroReminder", "quickCombat", "alwaysSkipCombat" ], "properties" : { "heroSpeed" : { "type" : "number", @@ -144,7 +144,13 @@ }, "quickCombat" : { "type" : "boolean", - "default" : true + "default" : true, + "description" : "enable to allow AI to play combats versus neutrals. Player can refuse battle result and replay it manually. This option can be switched from in-game menu" + }, + "alwaysSkipCombat" : { + "type" : "boolean", + "default" : false, + "description" : "if enabled, all battles will be controlled by AI" } } }, diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 13334afcd..aeb663cb2 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -741,9 +741,9 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) CasualtiesAfterBattle cab1(bEndArmy1, battleInfo), cab2(bEndArmy2, battleInfo); //calculate casualties before deleting battle ChangeSpells cs; //for Eagle Eye - if (finishingBattle->winnerHero) + if(!finishingBattle->isDraw() && finishingBattle->winnerHero) { - if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_VAL2, SecondarySkill::EAGLE_EYE)) + if(int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_VAL2, SecondarySkill::EAGLE_EYE)) { double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(Bonus::SECONDARY_SKILL_PREMY, SecondarySkill::EAGLE_EYE); for(auto & spellId : battleInfo->sides.at(!battleResult.data->winner).usedSpellsHistory) @@ -756,7 +756,7 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) } std::vector arts; //display them in window - if (result == BattleResult::NORMAL && finishingBattle->winnerHero) + if(result == BattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) { auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma) { @@ -890,6 +890,11 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) RemoveObject ro(finishingBattle->loserHero->id); sendAndApply(&ro); } + if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed + { + RemoveObject ro(finishingBattle->winnerHero->id); + sendAndApply(&ro); + } if(battleResult.data->winner == BattleSide::DEFENDER && finishingBattle->winnerHero @@ -900,10 +905,10 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place } //give exp - if(battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) + if(!finishingBattle->isDraw() && battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); - queries.popIfTop(battleQuery); + //queries.popIfTop(battleQuery); BattleResultAccepted raccepted; raccepted.army1 = const_cast(bEndArmy1); @@ -914,6 +919,7 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) raccepted.exp[1] = battleResult.data->exp[1]; sendAndApply(&raccepted); + queries.popIfTop(battleQuery); //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query (above) } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 2645d99b4..c475f4dc6 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -308,6 +308,8 @@ public: { FinishingBattleHelper(); FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); + + inline bool isDraw() const {return winnerSide == 2;} const CGHeroInstance *winnerHero, *loserHero; PlayerColor victor, loser; From e9e172164cd3a2b8038d785921b53600219d4dde Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Nov 2022 03:02:34 +0400 Subject: [PATCH 09/17] Implemented correct logic for battle queries --- server/CGameHandler.cpp | 30 +++++++++++++++++++++--------- server/CQuery.cpp | 7 ++++--- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index aeb663cb2..c82de25b3 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -713,6 +713,14 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con } battleQuery->result = boost::make_optional(*battleResult.data); + + //set same battle result for all queries + for(auto q : queries.allQueries()) + { + auto otherBattleQuery = std::dynamic_pointer_cast(q); + if(otherBattleQuery && otherBattleQuery->bi == battleQuery->bi) + otherBattleQuery->result = battleQuery->result; + } //Check how many battle queries were created (number of players blocked by battle) const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; @@ -729,8 +737,7 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(battleInfo->sides.at(0).color)); if(!battleQuery) { - logGlobal->error("Cannot find battle query!"); - complain("Player " + boost::lexical_cast(battleInfo->sides.at(0).color) + " has no battle query at the top!"); + logGlobal->trace("No battle query, battle end was confirmed by another player"); return; } @@ -908,8 +915,6 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) if(!finishingBattle->isDraw() && battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); - //queries.popIfTop(battleQuery); - BattleResultAccepted raccepted; raccepted.army1 = const_cast(bEndArmy1); raccepted.army2 = const_cast(bEndArmy2); @@ -919,8 +924,7 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) raccepted.exp[1] = battleResult.data->exp[1]; sendAndApply(&raccepted); - queries.popIfTop(battleQuery); - //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query (above) + //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query } void CGameHandler::battleAfterLevelUp(const BattleResult &result) @@ -2726,11 +2730,19 @@ 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 battleQuery = std::make_shared(this, gs->curB); - queries.addQuery(battleQuery); + //existing battle query for retying auto-combat + auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); + if(battleQuery) + { + battleQuery->bi = gs->curB; + battleQuery->result = boost::none; + battleQuery->belligerents[0] = gs->curB->sides[0].armyObject; + battleQuery->belligerents[1] = gs->curB->sides[1].armyObject; + } + + queries.addQuery(std::make_shared(this, gs->curB)); boost::thread(&CGameHandler::runBattle, this); } diff --git a/server/CQuery.cpp b/server/CQuery.cpp index ce7c91cc1..f484b112f 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -288,8 +288,8 @@ QueryPtr Queries::getQuery(QueryID queryID) void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { - assert(result); - objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); + if(result) + objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); } CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi): @@ -318,7 +318,8 @@ bool CBattleQuery::blocksPack(const CPack * pack) const void CBattleQuery::onRemoval(PlayerColor color) { - gh->battleAfterLevelUp(*result); + if(result) + gh->battleAfterLevelUp(*result); } void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const From bdc59b18ddbc83b840fc0b42734da1ad5d0ce8ff Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Nov 2022 18:40:14 +0400 Subject: [PATCH 10/17] Fix parameter name --- client/CPlayerInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 1885d0069..aabe321ce 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -694,7 +694,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet if((!replay && army2_object && army2_object->getOwner() == PlayerColor::UNFLAGGABLE && settings["adventure"]["quickCombat"].Bool()) - || settings["adventure"]["alwaysSkipBattle"].Bool()) + || settings["adventure"]["alwaysSkipCombat"].Bool()) { autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI->init(env, cb); From e865f484ffe99f0172008c5c9f96b10a602556d2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 30 Nov 2022 00:10:40 +0400 Subject: [PATCH 11/17] Fix some problems with network game --- client/CPlayerInterface.cpp | 6 ++++-- client/CPlayerInterface.h | 1 + server/CGameHandler.cpp | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index aabe321ce..05a203cb1 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -691,7 +691,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet lastBattleArmies.second = army2; //quick combat with neutral creatures only auto * army2_object = dynamic_cast(army2); - if((!replay && army2_object + if((!replay && !allowBattleReplay && army2_object && army2_object->getOwner() == PlayerColor::UNFLAGGABLE && settings["adventure"]["quickCombat"].Bool()) || settings["adventure"]["alwaysSkipCombat"].Bool()) @@ -703,6 +703,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet cb->registerBattleInterface(autofightingAI); // Player shouldn't be able to move on adventure map if quick combat is going adventureInt->quickCombatLock(); + allowBattleReplay = true; } //Don't wait for dialogs when we are non-active hot-seat player @@ -920,7 +921,8 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) if(!battleInt) { - bool replay = !settings["adventure"]["alwaysSkipCombat"].Bool(); //do not allow manual replay + bool replay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool(); //do not allow manual replay + allowBattleReplay = false; auto wnd = std::make_shared(*br, *this, replay); wnd->resultCallback = [=](ui32 selection) { diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 4f69f7abd..ed1f6e201 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -69,6 +69,7 @@ namespace boost class CPlayerInterface : public CGameInterface, public IUpdateable { const CArmedInstance * currentSelection; + bool allowBattleReplay = false; std::pair lastBattleArmies; public: diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index c82de25b3..6ece6182d 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -924,6 +924,7 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) raccepted.exp[1] = battleResult.data->exp[1]; sendAndApply(&raccepted); + queries.popIfTop(battleQuery); //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query } From ce3028bd73e4032af806b5e1c5f54f84e48998aa Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 6 Apr 2023 19:34:07 +0400 Subject: [PATCH 12/17] Intermediate commit --- AI/BattleAI/BattleAI.h | 2 +- AI/Nullkiller/AIGateway.cpp | 10 +++++-- AI/Nullkiller/AIGateway.h | 2 +- AI/StupidAI/StupidAI.cpp | 2 +- AI/StupidAI/StupidAI.h | 2 +- AI/VCAI/VCAI.cpp | 10 +++++-- AI/VCAI/VCAI.h | 2 +- client/CPlayerInterface.cpp | 24 ++++++++++++--- client/CPlayerInterface.h | 4 ++- client/Client.cpp | 38 +++++++++++++----------- client/NetPacksClient.cpp | 2 +- client/battle/BattleInterface.cpp | 11 +++++-- client/battle/BattleInterface.h | 2 +- client/battle/BattleInterfaceClasses.cpp | 21 +++++++++++-- client/battle/BattleInterfaceClasses.h | 7 ++++- lib/CGameInterface.cpp | 4 +-- lib/CGameInterface.h | 2 +- lib/IGameEventsReceiver.h | 2 +- lib/NetPacks.h | 24 +++++++++++++-- lib/NetPacksLib.cpp | 23 +++++++------- lib/battle/BattleInfo.cpp | 10 ++++++- lib/battle/BattleInfo.h | 1 + lib/registerTypes/RegisterTypes.h | 1 + server/CGameHandler.cpp | 24 +++++++++++++-- server/CGameHandler.h | 9 ++++-- server/CQuery.cpp | 29 ++++++++++++++++-- server/CQuery.h | 10 +++++++ 27 files changed, 214 insertions(+), 64 deletions(-) diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 679d54f0a..b9fec531c 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -81,7 +81,7 @@ public: //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 //void battleStacksAttacked(const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) - //void battleEnd(const BattleResult *br) override; + //void battleEnd(const BattleResult *br, QueryID queryID) override; //void battleResultsApplied() override; //called when all effects of last battle are applied //void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; //void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 516a33514..d35d8753e 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1090,7 +1090,7 @@ void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * arm CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); } -void AIGateway::battleEnd(const BattleResult * br) +void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) { NET_EVENT_HANDLER; assert(status.getBattle() == ONGOING_BATTLE); @@ -1098,7 +1098,13 @@ void AIGateway::battleEnd(const BattleResult * br) bool won = br->winner == myCb->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); - CAdventureAI::battleEnd(br); + status.addQuery(queryID, "Combat result dialog"); + const int confirmAction = 0; + requestActionASAP([=]() + { + answerQuery(queryID, confirmAction); + }); + CAdventureAI::battleEnd(br, queryID); } void AIGateway::waitTillFree() diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index 1443392f3..584d25873 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -169,7 +169,7 @@ public: boost::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 battleEnd(const BattleResult * br) override; + void battleEnd(const BattleResult * br, QueryID queryID) override; void makeTurn(); diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index ff0887c94..27b4f6253 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -184,7 +184,7 @@ void CStupidAI::battleStacksAttacked(const std::vector & bs print("battleStacksAttacked called"); } -void CStupidAI::battleEnd(const BattleResult *br) +void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID) { print("battleEnd called"); } diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 12481759a..172688627 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -32,7 +32,7 @@ public: void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack void battleStacksAttacked(const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) - void battleEnd(const BattleResult *br) override; + void battleEnd(const BattleResult *br, QueryID queryID) override; //void battleResultsApplied() override; //called when all effects of last battle are applied void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index d329c3a75..03d06d1e2 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1580,7 +1580,7 @@ void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, i CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); } -void VCAI::battleEnd(const BattleResult * br) +void VCAI::battleEnd(const BattleResult * br, QueryID queryID) { NET_EVENT_HANDLER; assert(status.getBattle() == ONGOING_BATTLE); @@ -1588,7 +1588,13 @@ void VCAI::battleEnd(const BattleResult * br) bool won = br->winner == myCb->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); - CAdventureAI::battleEnd(br); + status.addQuery(queryID, "Combat result dialog"); + const int confirmAction = 0; + requestActionASAP([=]() + { + answerQuery(queryID, confirmAction); + }); + CAdventureAI::battleEnd(br, queryID); } void VCAI::waitTillFree() diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 249a91189..d15933383 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -201,7 +201,7 @@ public: 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 battleEnd(const BattleResult * br) override; + void battleEnd(const BattleResult * br, QueryID queryID) override; void makeTurn(); void mainLoop(); diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 2c19b94ab..0d954f301 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -693,7 +693,15 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) { EVENT_HANDLER_CALLED_BY_CLIENT; - if (settings["adventure"]["quickCombat"].Bool()) + bool replay = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2); //will be true if player already refused auto-battle result + lastBattleArmies.first = army1; + lastBattleArmies.second = army2; + //quick combat with neutral creatures only + auto * army2_object = dynamic_cast(army2); + if((!replay && !allowBattleReplay && army2_object + && army2_object->getOwner() == PlayerColor::UNFLAGGABLE + && settings["adventure"]["quickCombat"].Bool()) + || settings["adventure"]["alwaysSkipCombat"].Bool()) { autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI->initBattleInterface(env, cb); @@ -702,6 +710,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet cb->registerBattleInterface(autofightingAI); // Player shouldn't be able to move on adventure map if quick combat is going adventureInt->quickCombatLock(); + allowBattleReplay = true; } //Don't wait for dialogs when we are non-active hot-seat player @@ -889,7 +898,7 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i return ret; } -void CPlayerInterface::battleEnd(const BattleResult *br) +void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; if(isAutoFightOn || autofightingAI) @@ -900,7 +909,14 @@ void CPlayerInterface::battleEnd(const BattleResult *br) if(!battleInt) { - GH.pushIntT(*br, *this); + bool replay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool(); //do not allow manual replay + allowBattleReplay = false; + auto wnd = std::make_shared(*br, *this, replay); + wnd->resultCallback = [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }; + GH.pushInt(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. waitWhileDialog(); @@ -911,7 +927,7 @@ void CPlayerInterface::battleEnd(const BattleResult *br) BATTLE_EVENT_POSSIBLE_RETURN; - battleInt->battleFinished(*br); + battleInt->battleFinished(*br, queryID); adventureInt->quickCombatUnlock(); } diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 591ecf260..deed92798 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -123,6 +123,8 @@ public: //During battle is quick combat mode is used std::shared_ptr autofightingAI; //AI that makes decisions bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface. + bool allowBattleReplay = false; + std::pair lastBattleArmies; struct SpellbookLastSetting { @@ -210,7 +212,7 @@ public: void actionStarted(const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack void battleAttack(const BattleAttack *ba) override; //stack performs attack - void battleEnd(const BattleResult *br) override; //end of battle + void battleEnd(const BattleResult *br, QueryID queryID) override; //end of battle void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; used for HP regen handling void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn void battleLogMessage(const std::vector & lines) override; diff --git a/client/Client.cpp b/client/Client.cpp index d153722af..d98aa3113 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -553,13 +553,29 @@ void CClient::battleStarted(const BattleInfo * info) auto & leftSide = info->sides[0], & rightSide = info->sides[1]; //If quick combat is not, do not prepare interfaces for battleint - if(!settings["adventure"]["quickCombat"].Bool()) + auto callBattleStart = [&](PlayerColor color, ui8 side) { - if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) - att = std::dynamic_pointer_cast(playerint[leftSide.color]); + if(vstd::contains(battleints, color)) + battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side); + }; + + callBattleStart(leftSide.color, 0); + callBattleStart(rightSide.color, 1); + callBattleStart(PlayerColor::UNFLAGGABLE, 1); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + callBattleStart(PlayerColor::SPECTATOR, 1); + + if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) + att = std::dynamic_pointer_cast(playerint[leftSide.color]); - if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) - def = std::dynamic_pointer_cast(playerint[rightSide.color]); + if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) + def = std::dynamic_pointer_cast(playerint[rightSide.color]); + + //Remove player interfaces for auto battle (quickCombat option) + if(att && att->isAutoFightOn) + { + att.reset(); + def.reset(); } if(!settings["session"]["headless"].Bool()) @@ -579,18 +595,6 @@ 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); - }; - - callBattleStart(leftSide.color, 0); - callBattleStart(rightSide.color, 1); - callBattleStart(PlayerColor::UNFLAGGABLE, 1); - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - callBattleStart(PlayerColor::SPECTATOR, 1); - if(info->tacticDistance && vstd::contains(battleints, info->sides[info->tacticsSide].color)) { boost::thread(&CClient::commenceTacticPhaseForInt, this, battleints[info->sides[info->tacticsSide].color]); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index d897b109a..1485b6d3b 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -735,7 +735,7 @@ void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGate void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, &pack); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, &pack, queryID); cl.battleFinished(); } diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index b1bc9ca93..0397956cb 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -308,7 +308,7 @@ void BattleInterface::gateStateChanged(const EGateState state) siegeController->gateStateChanged(state); } -void BattleInterface::battleFinished(const BattleResult& br) +void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID) { checkForAnimations(); stacksController->setActiveStack(nullptr); @@ -318,11 +318,18 @@ void BattleInterface::battleFinished(const BattleResult& br) if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool()) { + curInt->cb->selectionMade(0, queryID); windowObject->close(); return; } - GH.pushInt(std::make_shared(br, *(this->curInt))); + auto wnd = std::make_shared(br, *(this->curInt)); + wnd->resultCallback = [=](ui32 selection) + { + curInt->cb->selectionMade(selection, queryID); + }; + GH.pushInt(wnd); + curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 CPlayerInterface::battleInt = nullptr; } diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index a1abfb73e..44cb078af 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -200,7 +200,7 @@ public: void newRoundFirst( int round ); void newRound(int number); //caled when round is ended; number is the number of round void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls - void battleFinished(const BattleResult& br); //called when battle is finished - battleresult window should be printed + void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 7614b9e82..57378003d 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -397,7 +397,7 @@ HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) labels.push_back(std::make_shared(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints))); } -BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner) +BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay) : owner(_owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -408,6 +408,12 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface exit = std::make_shared(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, SDLK_RETURN); exit->setBorderColor(Colors::METALLIC_GOLD); + + if(allowReplay) + { + repeat = std::make_shared(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, SDLK_ESCAPE); + repeat->setBorderColor(Colors::METALLIC_GOLD); + } if(br.winner == 0) //attacker won { @@ -569,8 +575,9 @@ void BattleResultWindow::show(SDL_Surface * to) CCS->videoh->update(pos.x + 107, pos.y + 70, to, true, false); } -void BattleResultWindow::bExitf() +void BattleResultWindow::buttonPressed(int button) { + resultCallback(button); CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon close(); @@ -584,6 +591,16 @@ void BattleResultWindow::bExitf() CCS->videoh->close(); } +void BattleResultWindow::bExitf() +{ + buttonPressed(0); +} + +void BattleResultWindow::bRepeatf() +{ + buttonPressed(1); +} + StackQueue::StackQueue(bool Embedded, BattleInterface & owner) : embedded(Embedded), owner(owner) diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index f5a6909d8..00799bf22 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -143,13 +143,18 @@ private: std::shared_ptr background; std::vector> labels; std::shared_ptr exit; + std::shared_ptr repeat; std::vector> icons; std::shared_ptr description; CPlayerInterface & owner; + + void buttonPressed(int button); //internal function for button callbacks public: - BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner); + BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false); void bExitf(); //exit button callback + void bRepeatf(); //repeat button callback + std::function resultCallback; //callback receiving which button was pressed void activate() override; void show(SDL_Surface * to = 0) override; diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index 4e6d7a218..8f0018d23 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -230,9 +230,9 @@ void CAdventureAI::battleSpellCast(const BattleSpellCast * sc) battleAI->battleSpellCast(sc); } -void CAdventureAI::battleEnd(const BattleResult * br) +void CAdventureAI::battleEnd(const BattleResult * br, QueryID queryID) { - battleAI->battleEnd(br); + battleAI->battleEnd(br, queryID); battleAI.reset(); } diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index fee41e281..5fa0d0ab1 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -162,7 +162,7 @@ public: virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; virtual void battleAttack(const BattleAttack *ba) override; virtual void battleSpellCast(const BattleSpellCast *sc) override; - virtual void battleEnd(const BattleResult *br) override; + virtual void battleEnd(const BattleResult *br, QueryID queryID) override; virtual void battleUnitsChanged(const std::vector & units) override; virtual void saveGame(BinarySerializer & h, const int version) override; diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index a1da28237..37eb6b15b 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -60,7 +60,7 @@ public: virtual void actionStarted(const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero virtual void battleAttack(const BattleAttack *ba){}; //called when stack is performing attack virtual void battleStacksAttacked(const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) - virtual void battleEnd(const BattleResult *br){}; + virtual void battleEnd(const BattleResult *br, QueryID queryID){}; virtual void battleNewRoundFirst(int round){}; //called at the beginning of each turn before changes are applied; virtual void battleNewRound(int round){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn virtual void battleLogMessage(const std::vector & lines){}; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 6d57cc17d..6509910ba 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1466,12 +1466,31 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient } }; -struct DLL_LINKAGE BattleResult : public CPackForClient +struct DLL_LINKAGE BattleResultAccepted : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + CGHeroInstance * hero1 = nullptr; + CGHeroInstance * hero2 = nullptr; + CArmedInstance * army1 = nullptr; + CArmedInstance * army2 = nullptr; + TExpType exp[2]; + + template void serialize(Handler &h, const int version) + { + h & hero1; + h & hero2; + h & army1; + h & army2; + h & exp; + } +}; + +struct DLL_LINKAGE BattleResult : public Query { enum EResult { NORMAL = 0, ESCAPE = 1, SURRENDER = 2 }; void applyFirstCl(CClient * cl); - void applyGs(CGameState * gs); EResult result = NORMAL; ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] @@ -1483,6 +1502,7 @@ struct DLL_LINKAGE BattleResult : public CPackForClient template void serialize(Handler & h, const int version) { + h & queryID; h & result; h & winner; h & casualties[0]; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 6c46d04d2..f89f1ad7e 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2190,15 +2190,11 @@ void BattleUpdateGateState::applyGs(CGameState * gs) const gs->curB->si.gateState = state; } -void BattleResult::applyGs(CGameState *gs) +void BattleResultAccepted::applyGs(CGameState * gs) const { - for (auto & elem : gs->curB->stacks) - delete elem; - - - for(int i = 0; i < 2; ++i) + for(auto * h : {hero1, hero2}) { - if(auto * h = gs->curB->battleGetFightingHero(i)) + if(h) { h->removeBonusesRecursive(Bonus::OneBattle); //remove any "until next battle" bonuses if (h->commander && h->commander->alive) @@ -2214,15 +2210,20 @@ void BattleResult::applyGs(CGameState *gs) if(VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) { for(int i = 0; i < 2; i++) + { if(exp[i]) - gs->curB->battleGetArmyObject(i)->giveStackExp(exp[i]); + { + if(auto * army = (i == 0 ? army1 : army2)) + army->giveStackExp(exp[i]); + } + } CBonusSystemNode::treeHasChanged(); } - for(int i = 0; i < 2; i++) - gs->curB->battleGetArmyObject(i)->battle = nullptr; - + army1->battle = nullptr; + army2->battle = nullptr; + gs->curB.dellNull(); } diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index e6de1ba0a..65b0746d4 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -561,7 +561,15 @@ BattleInfo::BattleInfo(): setNodeType(BATTLE); } -BattleInfo::~BattleInfo() = default; +BattleInfo::~BattleInfo() +{ + for (auto & elem : stacks) + delete elem; + + for(int i = 0; i < 2; i++) + if(auto * _armyObj = battleGetArmyObject(i)) + _armyObj->battle = nullptr; +} int32_t BattleInfo::getActiveStackID() const { diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 02dcafe45..154c7d500 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -34,6 +34,7 @@ public: si32 round, activeStack; 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 std::vector stacks; std::vector > obstacles; SiegeInfo si; diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 58347a5b6..0c15bbe11 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -266,6 +266,7 @@ void registerTypesClientPacks2(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index e7538f1a3..0e12cb93d 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -832,6 +832,8 @@ void CGameHandler::battleAfterLevelUp(const BattleResult &result) { LOG_TRACE(logGlobal); + if(!finishingBattle) + return; finishingBattle->remainingBattleQueriesCount--; logGlobal->trace("Decremented queries count to %d", finishingBattle->remainingBattleQueriesCount); @@ -914,6 +916,8 @@ void CGameHandler::battleAfterLevelUp(const BattleResult &result) sendAndApply(&sah); } } + + finishingBattle.reset(); } void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) @@ -2627,6 +2631,9 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) //use hero=nullptr for no hero { + if(gs->curB) + gs->curB.dellNull(); + engageIntoBattle(army1->tempOwner); engageIntoBattle(army2->tempOwner); @@ -2640,8 +2647,17 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces - auto battleQuery = std::make_shared(this, gs->curB); - queries.addQuery(battleQuery); + //existing battle query for retying auto-combat + auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); + if(battleQuery) + { + battleQuery->bi = gs->curB; + battleQuery->result = boost::none; + battleQuery->belligerents[0] = gs->curB->sides[0].armyObject; + battleQuery->belligerents[1] = gs->curB->sides[1].armyObject; + } + + queries.addQuery(std::make_shared(this, gs->curB)); this->battleThread = std::make_unique(boost::thread(&CGameHandler::runBattle, this)); } @@ -7189,7 +7205,7 @@ void CGameHandler::showInfoDialog(const std::string & msg, PlayerColor player) showInfoDialog(&iw); } -CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, BattleInfo *bat): +CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance * _army, const BattleInfo * bat): army(_army) { heroWithDeadCommander = ObjectInstanceID(); @@ -7341,12 +7357,14 @@ CGameHandler::FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); + + inline bool isDraw() const {return winnerSide == 2;} const CGHeroInstance *winnerHero, *loserHero; PlayerColor victor, loser; + ui8 winnerSide; int remainingBattleQueriesCount; @@ -325,6 +329,7 @@ public: h & loserHero; h & victor; h & loser; + h & winnerSide; h & remainingBattleQueriesCount; } }; diff --git a/server/CQuery.cpp b/server/CQuery.cpp index cdfd90527..b8fc54bd9 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -288,8 +288,8 @@ QueryPtr Queries::getQuery(QueryID queryID) void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { - assert(result); - objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); + if(result) + objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); } CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi): @@ -318,7 +318,8 @@ bool CBattleQuery::blocksPack(const CPack * pack) const void CBattleQuery::onRemoval(PlayerColor color) { - gh->battleAfterLevelUp(*result); + if(result) + gh->battleAfterLevelUp(*result); } void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const @@ -386,6 +387,28 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const return CDialogQuery::blocksPack(pack); } +CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): + CDialogQuery(owner) +{ + bi = Bi; + + for(auto & side : bi->sides) + addPlayer(side.color); +} + +void CBattleDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + if(*answer == 1) + { + gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); + } + else + { + gh->endBattleConfirm(bi); + } +} + void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { assert(answer); diff --git a/server/CQuery.h b/server/CQuery.h index 5310c9109..809fa128d 100644 --- a/server/CQuery.h +++ b/server/CQuery.h @@ -145,6 +145,16 @@ public: virtual bool blocksPack(const CPack *pack) const override; }; +class CBattleDialogQuery : public CDialogQuery +{ +public: + CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi); + + const BattleInfo * bi; + + virtual void onRemoval(PlayerColor color) override; +}; + //yes/no and component selection dialogs class CBlockingDialogQuery : public CDialogQuery { From 3e28b58c80968a549c6ee401c4b102d2c1941b29 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 6 Apr 2023 19:59:22 +0400 Subject: [PATCH 13/17] Fixes --- client/NetPacksClient.cpp | 2 +- server/CQuery.cpp | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 1485b6d3b..d376d24bf 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -735,7 +735,7 @@ void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGate void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, &pack, queryID); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, &pack, pack.queryID); cl.battleFinished(); } diff --git a/server/CQuery.cpp b/server/CQuery.cpp index f0b97879c..de1fb8721 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -409,28 +409,6 @@ void CBattleDialogQuery::onRemoval(PlayerColor color) } } -CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): - CDialogQuery(owner) -{ - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); -} - -void CBattleDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - if(*answer == 1) - { - gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); - } - else - { - gh->endBattleConfirm(bi); - } -} - void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { assert(answer); From e85593dbb30f564a3975f19d5aa5811fee069f9a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 6 Apr 2023 21:19:46 +0400 Subject: [PATCH 14/17] Save & restore mana --- client/CPlayerInterface.cpp | 2 +- lib/NetPacksLib.cpp | 3 --- server/CGameHandler.cpp | 23 +++++++++++++++++++++-- server/CQuery.h | 1 + 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 0d954f301..cc930969e 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -911,7 +911,7 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) { bool replay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool(); //do not allow manual replay allowBattleReplay = false; - auto wnd = std::make_shared(*br, *this, replay); + auto wnd = std::make_shared(*br, *this, replay); wnd->resultCallback = [=](ui32 selection) { cb->selectionMade(selection, queryID); diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index f89f1ad7e..7707b57b0 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2221,9 +2221,6 @@ void BattleResultAccepted::applyGs(CGameState * gs) const CBonusSystemNode::treeHasChanged(); } - army1->battle = nullptr; - army2->battle = nullptr; - gs->curB.dellNull(); } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 5fe37342a..5efaec0d3 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -634,7 +634,7 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con //Check how many battle queries were created (number of players blocked by battle) const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; - finishingBattle = make_unique(battleQuery, queriedPlayers); + finishingBattle = std::make_unique(battleQuery, queriedPlayers); auto battleDialogQuery = std::make_shared(this, gs->curB); battleResult.data->queryID = battleDialogQuery->queryID; @@ -2667,13 +2667,32 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); if(battleQuery) { + for(int i : {0, 1}) + { + if(heroes[i]) + { + SetMana restoreInitialMana; + restoreInitialMana.val = battleQuery->initialHeroMana[i]; + restoreInitialMana.hid = heroes[i]->id; + sendAndApply(&restoreInitialMana); + } + } + battleQuery->bi = gs->curB; battleQuery->result = boost::none; battleQuery->belligerents[0] = gs->curB->sides[0].armyObject; battleQuery->belligerents[1] = gs->curB->sides[1].armyObject; } - queries.addQuery(std::make_shared(this, gs->curB)); + battleQuery = std::make_shared(this, gs->curB); + for(int i : {0, 1}) + { + if(heroes[i]) + { + battleQuery->initialHeroMana[i] = heroes[i]->mana; + } + } + queries.addQuery(battleQuery); this->battleThread = std::make_unique(boost::thread(&CGameHandler::runBattle, this)); } diff --git a/server/CQuery.h b/server/CQuery.h index 809fa128d..fae8c8d9a 100644 --- a/server/CQuery.h +++ b/server/CQuery.h @@ -97,6 +97,7 @@ class CBattleQuery : public CGhQuery { public: std::array belligerents; + std::array initialHeroMana; const BattleInfo *bi; boost::optional result; From b1d06b6141636291e2adf97789d13e8cb014e219 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 6 Apr 2023 22:49:38 +0400 Subject: [PATCH 15/17] Complete quick combat feature --- Mods/vcmi/config/vcmi/english.json | 2 ++ client/battle/BattleInterfaceClasses.cpp | 1 + client/widgets/TextControls.cpp | 4 ++-- server/CGameHandler.cpp | 17 +++++++++-------- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 1ae81fe31..a2f7362b8 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -94,6 +94,8 @@ "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", + "vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nShow the number of creatures available to purchase instead of their growth in town summary (bottom-left corner of town screen).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures", diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 57378003d..081efe24e 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -413,6 +413,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface { repeat = std::make_shared(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, SDLK_ESCAPE); repeat->setBorderColor(Colors::METALLIC_GOLD); + labels.push_back(std::make_shared(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel"))); } if(br.winner == 0) //attacker won diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 53c69c912..783615fcd 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -357,14 +357,14 @@ void CGStatusBar::setEnteringMode(bool on) if (on) { - assert(enteringText == false); + //assert(enteringText == false); alignment = ETextAlignment::TOPLEFT; GH.startTextInput(pos); setText(consoleText); } else { - assert(enteringText == true); + //assert(enteringText == true); alignment = ETextAlignment::CENTER; GH.stopTextInput(); setText(hoverText); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 5efaec0d3..ac7ccc20e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -623,14 +623,6 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con } battleQuery->result = boost::make_optional(*battleResult.data); - - //set same battle result for all queries - for(auto q : queries.allQueries()) - { - auto otherBattleQuery = std::dynamic_pointer_cast(q); - if(otherBattleQuery && otherBattleQuery->bi == battleQuery->bi) - otherBattleQuery->result = battleQuery->result; - } //Check how many battle queries were created (number of players blocked by battle) const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; @@ -639,6 +631,15 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con auto battleDialogQuery = std::make_shared(this, gs->curB); battleResult.data->queryID = battleDialogQuery->queryID; queries.addQuery(battleDialogQuery); + + //set same battle result for all queries + for(auto q : queries.allQueries()) + { + auto otherBattleQuery = std::dynamic_pointer_cast(q); + if(otherBattleQuery) + otherBattleQuery->result = battleQuery->result; + } + sendAndApply(battleResult.data); //after this point casualties objects are destroyed } From 50115c3113473f27e7e506ae06bb2c92cf961caa Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 8 Apr 2023 20:56:05 +0400 Subject: [PATCH 16/17] Rename variables --- client/CPlayerInterface.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index cc930969e..40a11151a 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -693,12 +693,12 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) { EVENT_HANDLER_CALLED_BY_CLIENT; - bool replay = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2); //will be true if player already refused auto-battle result + 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((!replay && !allowBattleReplay && army2_object + if((!autoBattleResultRefused && !allowBattleReplay && army2_object && army2_object->getOwner() == PlayerColor::UNFLAGGABLE && settings["adventure"]["quickCombat"].Bool()) || settings["adventure"]["alwaysSkipCombat"].Bool()) @@ -909,9 +909,9 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) if(!battleInt) { - bool replay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool(); //do not allow manual replay + bool allowManualReplay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool(); allowBattleReplay = false; - auto wnd = std::make_shared(*br, *this, replay); + auto wnd = std::make_shared(*br, *this, allowManualReplay); wnd->resultCallback = [=](ui32 selection) { cb->selectionMade(selection, queryID); From 39f4bc0072ae0f537676b572b8d28fbb35194d9a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 11 Apr 2023 17:25:19 +0400 Subject: [PATCH 17/17] Reset battle bonuses on client side --- client/Client.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/client/Client.cpp b/client/Client.cpp index d98aa3113..22dfcc367 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -630,6 +630,7 @@ void CClient::battleFinished() battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr); setBattle(nullptr); + gs->curB.dellNull(); } void CClient::startPlayerBattleAction(PlayerColor color)