1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-24 22:14:36 +02:00

Minor rework & cleanup of combat replays

This commit is contained in:
Ivan Savenko 2023-07-27 18:40:47 +03:00
parent b8110218c0
commit aed8c411fc
20 changed files with 90 additions and 67 deletions

View File

@ -826,7 +826,7 @@ void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcas
ps.value = totalGain; ps.value = totalGain;
} }
void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{ {
LOG_TRACE(logAi); LOG_TRACE(logAi);
side = Side; side = Side;

View File

@ -83,7 +83,7 @@ public:
BattleAction selectStackAction(const CStack * stack); BattleAction selectStackAction(const CStack * stack);
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack *stack); std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack *stack);
void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side) override; void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override;
//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void 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 battleAttack(const BattleAttack *ba) override; //called when stack is performing attack

View File

@ -809,7 +809,7 @@ void AIGateway::makeTurn()
for (auto h : cb->getHeroesInfo()) for (auto h : cb->getHeroesInfo())
{ {
if (h->movementPointsRemaining()) if (h->movementPointsRemaining())
logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
} }
#if NKAI_TRACE_LEVEL == 0 #if NKAI_TRACE_LEVEL == 0
} }
@ -1065,14 +1065,14 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
} }
} }
void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
{ {
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
status.setBattle(ONGOING_BATTLE); status.setBattle(ONGOING_BATTLE);
const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
} }
void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
@ -1083,12 +1083,16 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
bool won = br->winner == myCb->battleGetMySide(); bool won = br->winner == myCb->battleGetMySide();
logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
battlename.clear(); battlename.clear();
status.addQuery(queryID, "Combat result dialog");
const int confirmAction = 0; if (queryID != -1)
requestActionASAP([=]()
{ {
answerQuery(queryID, confirmAction); status.addQuery(queryID, "Combat result dialog");
}); const int confirmAction = 0;
requestActionASAP([=]()
{
answerQuery(queryID, confirmAction);
});
}
CAdventureAI::battleEnd(br, queryID); CAdventureAI::battleEnd(br, queryID);
} }
@ -1175,7 +1179,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
if(startHpos == dst) if(startHpos == dst)
{ {
//FIXME: this assertion fails also if AI moves onto defeated guarded object //FIXME: this assertion fails also if AI moves onto defeated guarded object
assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object //assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
cb->moveHero(*h, h->convertFromVisitablePos(dst)); cb->moveHero(*h, h->convertFromVisitablePos(dst));
afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly? afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared // If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared

View File

@ -169,7 +169,7 @@ public:
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override; void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
void battleEnd(const BattleResult * br, QueryID queryID) override; void battleEnd(const BattleResult * br, QueryID queryID) override;
void makeTurn(); void makeTurn();

View File

@ -242,7 +242,7 @@ void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse)
print("battleStacksEffectsSet called"); print("battleStacksEffectsSet called");
} }
void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side) void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{ {
print("battleStart called"); print("battleStart called");
side = Side; side = Side;

View File

@ -44,7 +44,7 @@ public:
void battleSpellCast(const BattleSpellCast *sc) override; void battleSpellCast(const BattleSpellCast *sc) override;
void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override; //void battleTriggerEffect(const BattleTriggerEffect & bte) override;
void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
private: private:

View File

@ -819,7 +819,7 @@ void VCAI::makeTurn()
for (auto h : cb->getHeroesInfo()) for (auto h : cb->getHeroesInfo())
{ {
if (h->movementPointsRemaining()) if (h->movementPointsRemaining())
logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
} }
} }
catch (boost::thread_interrupted & e) catch (boost::thread_interrupted & e)
@ -1575,14 +1575,14 @@ void VCAI::completeGoal(Goals::TSubgoal goal)
} }
void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
{ {
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
status.setBattle(ONGOING_BATTLE); status.setBattle(ONGOING_BATTLE);
const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side); CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
} }
void VCAI::battleEnd(const BattleResult * br, QueryID queryID) void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
@ -1593,12 +1593,16 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
bool won = br->winner == myCb->battleGetMySide(); bool won = br->winner == myCb->battleGetMySide();
logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
battlename.clear(); battlename.clear();
status.addQuery(queryID, "Combat result dialog");
const int confirmAction = 0; if (queryID != -1)
requestActionASAP([=]()
{ {
answerQuery(queryID, confirmAction); status.addQuery(queryID, "Combat result dialog");
}); const int confirmAction = 0;
requestActionASAP([=]()
{
answerQuery(queryID, confirmAction);
});
}
CAdventureAI::battleEnd(br, queryID); CAdventureAI::battleEnd(br, queryID);
} }

View File

@ -201,7 +201,7 @@ public:
void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override; void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override; void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
void battleEnd(const BattleResult * br, QueryID queryID) override; void battleEnd(const BattleResult * br, QueryID queryID) override;
void makeTurn(); void makeTurn();

View File

@ -652,26 +652,20 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat
waitForAllDialogs(); waitForAllDialogs();
} }
void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed)
{ {
EVENT_HANDLER_CALLED_BY_CLIENT; EVENT_HANDLER_CALLED_BY_CLIENT;
bool autoBattleResultRefused = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2);
lastBattleArmies.first = army1; bool useQuickCombat = settings["adventure"]["quickCombat"].Bool();
lastBattleArmies.second = army2; bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool();
//quick combat with neutral creatures only
auto * army2_object = dynamic_cast<const CGObjectInstance *>(army2); if ((replayAllowed && useQuickCombat) || forceQuickCombat)
if((!autoBattleResultRefused && !allowBattleReplay && army2_object
&& (army2_object->getOwner() == PlayerColor::UNFLAGGABLE || army2_object->getOwner() == PlayerColor::NEUTRAL)
&& settings["adventure"]["quickCombat"].Bool())
|| settings["adventure"]["alwaysSkipCombat"].Bool())
{ {
autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
autofightingAI->initBattleInterface(env, cb); autofightingAI->initBattleInterface(env, cb);
autofightingAI->battleStart(army1, army2, int3(0,0,0), hero1, hero2, side); autofightingAI->battleStart(army1, army2, tile, hero1, hero2, side, false);
isAutoFightOn = true; isAutoFightOn = true;
cb->registerBattleInterface(autofightingAI); cb->registerBattleInterface(autofightingAI);
// Player shouldn't be able to move on adventure map if quick combat is going
allowBattleReplay = true;
} }
//Don't wait for dialogs when we are non-active hot-seat player //Don't wait for dialogs when we are non-active hot-seat player
@ -843,13 +837,17 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID)
if(!battleInt) if(!battleInt)
{ {
bool allowManualReplay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool(); bool allowManualReplay = queryID != -1;
allowBattleReplay = false;
auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay); auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
wnd->resultCallback = [=](ui32 selection)
if (allowManualReplay)
{ {
cb->selectionMade(selection, queryID); wnd->resultCallback = [=](ui32 selection)
}; {
cb->selectionMade(selection, queryID);
};
}
GH.windows().pushWindow(wnd); GH.windows().pushWindow(wnd);
// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. // #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. // Otherwise NewTurn causes freeze.

View File

@ -65,8 +65,6 @@ class CPlayerInterface : public CGameInterface, public IUpdateable
int firstCall; int firstCall;
int autosaveCount; int autosaveCount;
std::pair<const CCreatureSet *, const CCreatureSet *> lastBattleArmies;
bool allowBattleReplay = false;
std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr) const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr)
@ -169,7 +167,7 @@ protected: // Call-ins from server, should not be called directly, but only via
void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect
void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleUnitsChanged(const std::vector<UnitChanges> & units) override; void battleUnitsChanged(const std::vector<UnitChanges> & units) override;
void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override; void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override;
void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack

View File

@ -580,7 +580,7 @@ void CClient::battleStarted(const BattleInfo * info)
auto callBattleStart = [&](PlayerColor color, ui8 side) auto callBattleStart = [&](PlayerColor color, ui8 side)
{ {
if(vstd::contains(battleints, color)) if(vstd::contains(battleints, color))
battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side); battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
}; };
callBattleStart(leftSide.color, 0); callBattleStart(leftSide.color, 0);

View File

@ -501,7 +501,7 @@ void BattleWindow::bAutofightf()
auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
ai->initBattleInterface(owner.curInt->env, owner.curInt->cb); ai->initBattleInterface(owner.curInt->env, owner.curInt->cb);
ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide()); ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide(), false);
owner.curInt->autofightingAI = ai; owner.curInt->autofightingAI = ai;
owner.curInt->cb->registerBattleInterface(ai); owner.curInt->cb->registerBattleInterface(ai);

View File

@ -223,7 +223,7 @@
"type" : "object", "type" : "object",
"additionalProperties" : false, "additionalProperties" : false,
"default" : {}, "default" : {},
"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "alwaysSkipCombat", "borderScroll", "leftButtonDrag" ], "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag" ],
"properties" : { "properties" : {
"heroMoveTime" : { "heroMoveTime" : {
"type" : "number", "type" : "number",
@ -253,7 +253,7 @@
"type" : "boolean", "type" : "boolean",
"default" : true "default" : true
}, },
"alwaysSkipCombat" : { "forceQuickCombat" : {
"type" : "boolean", "type" : "boolean",
"default" : false "default" : false
}, },

View File

@ -168,13 +168,13 @@ void CAdventureAI::battleCatapultAttacked(const CatapultAttack & ca)
} }
void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile,
const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
{ {
assert(!battleAI); assert(!battleAI);
assert(cbc); assert(cbc);
battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName()); battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName());
battleAI->initBattleInterface(env, cbc); battleAI->initBattleInterface(env, cbc);
battleAI->battleStart(army1, army2, tile, hero1, hero2, side); battleAI->battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
} }
void CAdventureAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) void CAdventureAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)

View File

@ -149,7 +149,7 @@ public:
virtual void yourTacticPhase(int distance) override; virtual void yourTacticPhase(int distance) override;
virtual void battleNewRound(int round) override; virtual void battleNewRound(int round) override;
virtual void battleCatapultAttacked(const CatapultAttack & ca) override; virtual void battleCatapultAttacked(const CatapultAttack & ca) override;
virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override;
virtual void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; virtual void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
virtual void actionStarted(const BattleAction &action) override; virtual void actionStarted(const BattleAction &action) override;
virtual void battleNewRoundFirst(int round) override; virtual void battleNewRoundFirst(int round) override;

View File

@ -69,7 +69,7 @@ public:
virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks
virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects
virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start
virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side){}; //called by engine when battle starts; side=0 - left, side=1 - right virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right
virtual void battleUnitsChanged(const std::vector<UnitChanges> & units){}; virtual void battleUnitsChanged(const std::vector<UnitChanges> & units){};
virtual void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles){}; virtual void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles){};
virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack

View File

@ -207,6 +207,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
curB->round = -2; curB->round = -2;
curB->activeStack = -1; curB->activeStack = -1;
curB->creatureBank = creatureBank; curB->creatureBank = creatureBank;
curB->replayAllowed = false;
if(town) if(town)
{ {

View File

@ -36,6 +36,7 @@ public:
const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege) 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 int3 tile; //for background and bonuses
bool creatureBank; //auxilary field, do not serialize bool creatureBank; //auxilary field, do not serialize
bool replayAllowed;
std::vector<CStack*> stacks; std::vector<CStack*> stacks;
std::vector<std::shared_ptr<CObstacleInstance> > obstacles; std::vector<std::shared_ptr<CObstacleInstance> > obstacles;
SiegeInfo si; SiegeInfo si;
@ -61,6 +62,10 @@ public:
h & tacticsSide; h & tacticsSide;
h & tacticDistance; h & tacticDistance;
h & static_cast<CBonusSystemNode&>(*this); h & static_cast<CBonusSystemNode&>(*this);
if (version > 824)
h & replayAllowed;
else
replayAllowed = false;
} }
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////

View File

@ -14,7 +14,7 @@
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
const ui32 SERIALIZATION_VERSION = 824; const ui32 SERIALIZATION_VERSION = 825;
const ui32 MINIMAL_SERIALIZATION_VERSION = 824; const ui32 MINIMAL_SERIALIZATION_VERSION = 824;
const std::string SAVEGAME_MAGIC = "VCMISVG"; const std::string SAVEGAME_MAGIC = "VCMISVG";

View File

@ -607,9 +607,15 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0;
finishingBattle = std::make_unique<FinishingBattleHelper>(battleQuery, queriedPlayers); finishingBattle = std::make_unique<FinishingBattleHelper>(battleQuery, queriedPlayers);
auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(this, gs->curB); // in battles against neutrals, 1st player can ask to replay battle manually
battleResult.data->queryID = battleDialogQuery->queryID; if (!gs->curB->sides[1].color.isValidPlayer())
queries.addQuery(battleDialogQuery); {
auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(this, gs->curB);
battleResult.data->queryID = battleDialogQuery->queryID;
queries.addQuery(battleDialogQuery);
}
else
battleResult.data->queryID = -1;
//set same battle result for all queries //set same battle result for all queries
for(auto q : queries.allQueries()) for(auto q : queries.allQueries())
@ -620,6 +626,9 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
} }
sendAndApply(battleResult.data); //after this point casualties objects are destroyed sendAndApply(battleResult.data); //after this point casualties objects are destroyed
if (battleResult.data->queryID == -1)
endBattleConfirm(gs->curB);
} }
void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo)
@ -2118,6 +2127,10 @@ void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const
//send info about battles //send info about battles
BattleStart bs; BattleStart bs;
bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town);
auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(queries.topQuery(bs.info->sides[0].color));
bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer();
sendAndApply(&bs); sendAndApply(&bs);
} }
@ -2587,39 +2600,39 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI
heroes[0] = hero1; heroes[0] = hero1;
heroes[1] = hero2; heroes[1] = hero2;
setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces
auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(queries.topQuery(gs->curB->sides[0].color));
//existing battle query for retying auto-combat //existing battle query for retying auto-combat
auto battleQuery = std::dynamic_pointer_cast<CBattleQuery>(queries.topQuery(gs->curB->sides[0].color)); if(lastBattleQuery)
if(battleQuery)
{ {
for(int i : {0, 1}) for(int i : {0, 1})
{ {
if(heroes[i]) if(heroes[i])
{ {
SetMana restoreInitialMana; SetMana restoreInitialMana;
restoreInitialMana.val = battleQuery->initialHeroMana[i]; restoreInitialMana.val = lastBattleQuery->initialHeroMana[i];
restoreInitialMana.hid = heroes[i]->id; restoreInitialMana.hid = heroes[i]->id;
sendAndApply(&restoreInitialMana); sendAndApply(&restoreInitialMana);
} }
} }
battleQuery->bi = gs->curB; lastBattleQuery->bi = gs->curB;
battleQuery->result = std::nullopt; lastBattleQuery->result = std::nullopt;
battleQuery->belligerents[0] = gs->curB->sides[0].armyObject; lastBattleQuery->belligerents[0] = gs->curB->sides[0].armyObject;
battleQuery->belligerents[1] = gs->curB->sides[1].armyObject; lastBattleQuery->belligerents[1] = gs->curB->sides[1].armyObject;
} }
battleQuery = std::make_shared<CBattleQuery>(this, gs->curB); auto nextBattleQuery = std::make_shared<CBattleQuery>(this, gs->curB);
for(int i : {0, 1}) for(int i : {0, 1})
{ {
if(heroes[i]) if(heroes[i])
{ {
battleQuery->initialHeroMana[i] = heroes[i]->mana; nextBattleQuery->initialHeroMana[i] = heroes[i]->mana;
} }
} }
queries.addQuery(battleQuery); queries.addQuery(nextBattleQuery);
this->battleThread = std::make_unique<boost::thread>(boost::thread(&CGameHandler::runBattle, this)); this->battleThread = std::make_unique<boost::thread>(boost::thread(&CGameHandler::runBattle, this));
} }