Explorar el Código

Merge pull request #3840 from IvanSavenko/enemy_turn_interaction

Allow limited interaction with map during enemy turn
Ivan Savenko hace 1 año
padre
commit
2508772db6

+ 0 - 1
client/CPlayerInterface.cpp

@@ -1215,7 +1215,6 @@ void CPlayerInterface::loadGame( BinaryDeserializer & h )
 
 void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path )
 {
-	assert(LOCPLINT->makingTurn);
 	assert(h);
 	assert(!showingDialog->get());
 	assert(dialogs.empty());

+ 0 - 3
client/adventureMap/AdventureMapInterface.cpp

@@ -510,9 +510,6 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 	if(!shortcuts->optionMapViewActive())
 		return;
 
-	if(!LOCPLINT->makingTurn)
-		return;
-
 	const CGObjectInstance *topBlocking = LOCPLINT->cb->isVisible(targetPosition) ? getActiveObject(targetPosition) : nullptr;
 
 	if(spellBeingCasted)

+ 17 - 3
client/adventureMap/AdventureMapShortcuts.cpp

@@ -64,6 +64,8 @@ std::vector<AdventureMapShortcutState> AdventureMapShortcuts::getShortcuts()
 		{ EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL, optionCanToggleLevel(), [this]() { this->switchMapLevel(); } },
 		{ EShortcut::ADVENTURE_QUEST_LOG,        optionCanViewQuests(),  [this]() { this->showQuestlog(); } },
 		{ EShortcut::ADVENTURE_TOGGLE_SLEEP,     optionHeroSelected(),   [this]() { this->toggleSleepWake(); } },
+		{ EShortcut::ADVENTURE_TOGGLE_GRID,      optionInMapView(),      [this]() { this->toggleGrid(); } },
+		{ EShortcut::ADVENTURE_TRACK_HERO,       optionInMapView(),      [this]() { this->toggleTrackHero(); } },
 		{ EShortcut::ADVENTURE_SET_HERO_ASLEEP,  optionHeroAwake(),      [this]() { this->setHeroSleeping(); } },
 		{ EShortcut::ADVENTURE_SET_HERO_AWAKE,   optionHeroSleeping(),   [this]() { this->setHeroAwake(); } },
 		{ EShortcut::ADVENTURE_MOVE_HERO,        optionHeroCanMove(),    [this]() { this->moveHeroAlongPath(); } },
@@ -143,6 +145,18 @@ void AdventureMapShortcuts::showQuestlog()
 	LOCPLINT->showQuestLog();
 }
 
+void AdventureMapShortcuts::toggleTrackHero()
+{
+	Settings s = settings.write["session"];
+	s["adventureTrackHero"].Bool() = !settings["session"]["adventureTrackHero"].Bool();
+}
+
+void AdventureMapShortcuts::toggleGrid()
+{
+	Settings s = settings.write["gameTweaks"];
+	s["showGrid"].Bool() = !settings["gameTweaks"]["showGrid"].Bool();
+}
+
 void AdventureMapShortcuts::toggleSleepWake()
 {
 	if (!optionHeroSelected())
@@ -475,16 +489,16 @@ bool AdventureMapShortcuts::optionInWorldView()
 
 bool AdventureMapShortcuts::optionSidePanelActive()
 {
-	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
+	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
 }
 
 bool AdventureMapShortcuts::optionMapScrollingActive()
 {
-	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || (state == EAdventureState::OTHER_HUMAN_PLAYER_TURN);
+	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
 }
 
 bool AdventureMapShortcuts::optionMapViewActive()
 {
 	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL
-		|| (state == EAdventureState::OTHER_HUMAN_PLAYER_TURN);
+		|| state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
 }

+ 2 - 0
client/adventureMap/AdventureMapShortcuts.h

@@ -40,6 +40,8 @@ class AdventureMapShortcuts
 	void worldViewScale4x();
 	void switchMapLevel();
 	void showQuestlog();
+	void toggleTrackHero();
+	void toggleGrid();
 	void toggleSleepWake();
 	void setHeroSleeping();
 	void setHeroAwake();

+ 16 - 23
client/eventsSDL/InputSourceKeyboard.cpp

@@ -15,6 +15,7 @@
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/EventDispatcher.h"
+#include "../gui/Shortcut.h"
 #include "../gui/ShortcutHandler.h"
 #include "../CServerHandler.h"
 #include "../globalLobby/GlobalLobbyClient.h"
@@ -57,37 +58,29 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 			return; // ignore periodic event resends
 	}
 
+	auto shortcutsVector = GH.shortcuts().translateKeycode(keyName);
 
-	if(key.keysym.sym == SDLK_TAB && isKeyboardCtrlDown())
-	{
+	if (vstd::contains(shortcutsVector, EShortcut::LOBBY_ACTIVATE_INTERFACE))
 		CSH->getGlobalLobby().activateInterface();
+
+	if (vstd::contains(shortcutsVector, EShortcut::SPECTATE_TRACK_HERO))
+	{
+		Settings s = settings.write["session"];
+		s["spectate-ignore-hero"].Bool() = !settings["session"]["spectate-ignore-hero"].Bool();
 	}
 
-	if(key.keysym.sym >= SDLK_F1 && key.keysym.sym <= SDLK_F15 && settings["session"]["spectate"].Bool())
+	if (vstd::contains(shortcutsVector, EShortcut::SPECTATE_SKIP_BATTLE))
 	{
-		//TODO: we need some central place for all interface-independent hotkeys
 		Settings s = settings.write["session"];
-		switch(key.keysym.sym)
-		{
-			case SDLK_F6:
-				s["spectate-ignore-hero"].Bool() = !settings["session"]["spectate-ignore-hero"].Bool();
-				break;
-
-			case SDLK_F7:
-				s["spectate-skip-battle"].Bool() = !settings["session"]["spectate-skip-battle"].Bool();
-				break;
-
-			case SDLK_F8:
-				s["spectate-skip-battle-result"].Bool() = !settings["session"]["spectate-skip-battle-result"].Bool();
-				break;
-
-			default:
-				break;
-		}
-		return;
+		s["spectate-skip-battle"].Bool() = !settings["session"]["spectate-skip-battle"].Bool();
+	}
+
+	if (vstd::contains(shortcutsVector, EShortcut::SPECTATE_SKIP_BATTLE_RESULT))
+	{
+		Settings s = settings.write["session"];
+		s["spectate-skip-battle-result"].Bool() = !settings["session"]["spectate-skip-battle-result"].Bool();
 	}
 
-	auto shortcutsVector = GH.shortcuts().translateKeycode(keyName);
 	GH.events().dispatchShortcutPressed(shortcutsVector);
 }
 

+ 7 - 0
client/gui/Shortcut.h

@@ -108,6 +108,7 @@ enum class EShortcut
 	ADVENTURE_VIEW_WORLD_X1,
 	ADVENTURE_VIEW_WORLD_X2,
 	ADVENTURE_VIEW_WORLD_X4,
+	ADVENTURE_TRACK_HERO,
 	ADVENTURE_TOGGLE_MAP_LEVEL,
 	ADVENTURE_KINGDOM_OVERVIEW,
 	ADVENTURE_QUEST_LOG,
@@ -145,6 +146,12 @@ enum class EShortcut
 	BATTLE_SELECT_ACTION, // Alternative actions toggle
 	BATTLE_TOGGLE_HEROES_STATS,
 
+	LOBBY_ACTIVATE_INTERFACE,
+
+	SPECTATE_TRACK_HERO,
+	SPECTATE_SKIP_BATTLE,
+	SPECTATE_SKIP_BATTLE_RESULT,
+
 	// Town screen
 	TOWN_OPEN_TAVERN,
 	TOWN_SWAP_ARMIES, // Swap garrisoned and visiting armies

+ 5 - 0
client/gui/ShortcutHandler.cpp

@@ -176,6 +176,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"adventureViewWorld1",      EShortcut::ADVENTURE_VIEW_WORLD_X1   },
 		{"adventureViewWorld2",      EShortcut::ADVENTURE_VIEW_WORLD_X2   },
 		{"adventureViewWorld4",      EShortcut::ADVENTURE_VIEW_WORLD_X4   },
+		{"adventureTrackHero",       EShortcut::ADVENTURE_TRACK_HERO,     },
 		{"adventureToggleMapLevel",  EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL},
 		{"adventureKingdomOverview", EShortcut::ADVENTURE_KINGDOM_OVERVIEW},
 		{"adventureQuestLog",        EShortcut::ADVENTURE_QUEST_LOG       },
@@ -200,6 +201,10 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"battleTacticsNext",        EShortcut::BATTLE_TACTICS_NEXT       },
 		{"battleTacticsEnd",         EShortcut::BATTLE_TACTICS_END        },
 		{"battleSelectAction",       EShortcut::BATTLE_SELECT_ACTION      },
+		{"lobbyActivateInterface",   EShortcut::LOBBY_ACTIVATE_INTERFACE  },
+		{"spectateTrackHero",        EShortcut::SPECTATE_TRACK_HERO       },
+		{"spectateSkipBattle",       EShortcut::SPECTATE_SKIP_BATTLE      },
+		{"spectateSkipBattleResult", EShortcut::SPECTATE_SKIP_BATTLE_RESULT },
 		{"townOpenTavern",           EShortcut::TOWN_OPEN_TAVERN          },
 		{"townSwapArmies",           EShortcut::TOWN_SWAP_ARMIES          },
 		{"recruitmentMax",           EShortcut::RECRUITMENT_MAX           },

+ 18 - 6
client/mapView/MapViewController.cpp

@@ -16,6 +16,7 @@
 #include "MapViewCache.h"
 #include "MapViewModel.h"
 
+#include "../CCallback.h"
 #include "../CPlayerInterface.h"
 #include "../adventureMap/AdventureMapInterface.h"
 #include "../gui/CGuiHandler.h"
@@ -23,6 +24,7 @@
 #include "../eventsSDL/InputHandler.h"
 
 #include "../../lib/CConfigHandler.h"
+#include "../../lib/StartInfo.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/MiscObjects.h"
 #include "../../lib/pathfinder/CGPathNode.h"
@@ -295,9 +297,14 @@ bool MapViewController::isEventVisible(const CGObjectInstance * obj, const Playe
 	if(!GH.windows().isTopWindow(adventureInt))
 		return false;
 
-	// do not focus on actions of other players during our turn (e.g. simturns)
-	if (LOCPLINT->makingTurn && initiator != LOCPLINT->playerID)
-		return false;
+	// do not focus on actions of other players except for AI with simturns off
+	if (initiator != LOCPLINT->playerID)
+	{
+		if (LOCPLINT->makingTurn)
+			return false;
+		if (LOCPLINT->cb->getStartInfo()->playerInfos.at(initiator).isControlledByHuman() && !settings["session"]["adventureTrackHero"].Bool())
+			return false;
+	}
 
 	if(obj->isVisitable())
 		return context->isVisible(obj->visitablePos());
@@ -316,9 +323,14 @@ bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 &
 	if(!GH.windows().isTopWindow(adventureInt))
 		return false;
 
-	// do not focus on actions of other players during our turn (e.g. simturns)
-	if (LOCPLINT->makingTurn && obj->getOwner() != LOCPLINT->playerID)
-		return false;
+	// do not focus on actions of other players except for AI with simturns off
+	if (obj->getOwner() != LOCPLINT->playerID)
+	{
+		if (LOCPLINT->makingTurn)
+			return false;
+		if (LOCPLINT->cb->getStartInfo()->playerInfos.at(obj->getOwner()).isControlledByHuman() && !settings["session"]["adventureTrackHero"].Bool())
+			return false;
+	}
 
 	if(context->isVisible(obj->convertToVisitablePos(from)))
 		return true;

+ 9 - 1
client/widgets/CGarrisonInt.cpp

@@ -269,14 +269,22 @@ bool CGarrisonSlot::mustForceReselection() const
 {
 	const CGarrisonSlot * selection = owner->getSelection();
 	bool withAlly = selection->our() ^ our();
+
+	// not our turn - actions are blocked
+	if (!LOCPLINT->makingTurn)
+		return true;
+
 	if (!creature || !selection->creature)
 		return false;
+
 	// Attempt to take creatures from ally (select theirs first)
 	if (!selection->our())
 		return true;
+
 	// Attempt to swap creatures with ally (select ours first)
 	if (selection->creature != creature && withAlly)
 		return true;
+
 	if (!owner->removableUnits)
 	{
 		if (selection->upg == EGarrisonType::UPPER)
@@ -315,7 +323,7 @@ void CGarrisonSlot::clickPressed(const Point & cursorPosition)
 		{
 			if(creature)
 				owner->selectSlot(this);
-			redraw();
+			owner->redraw();
 			refr = true;
 		}
 		else

+ 2 - 2
client/widgets/markets/CAltarArtifacts.cpp

@@ -171,7 +171,7 @@ void CAltarArtifacts::updateAltarSlots()
 	}
 
 	calcExpAltarForHero();
-	deal->block(tradeSlotsMap.empty());
+	deal->block(tradeSlotsMap.empty() || !LOCPLINT->makingTurn);
 }
 
 void CAltarArtifacts::putBackArtifacts()
@@ -205,7 +205,7 @@ void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr<CTradeableItem> &
 			{
 				if(altarSlot->id == -1)
 					tradeSlotsMap.try_emplace(altarSlot, pickedArtInst);
-				deal->block(false);
+				deal->block(!LOCPLINT->makingTurn);
 
 				LOCPLINT->cb->swapArtifacts(ArtifactLocation(heroArts->getHero()->id, ArtifactPosition::TRANSITION_POS),
 					ArtifactLocation(altarId, ArtifactPosition::ALTAR));

+ 2 - 2
client/widgets/markets/CAltarCreatures.cpp

@@ -205,7 +205,7 @@ void CAltarCreatures::sacrificeAll()
 	offerTradePanel->update();
 	updateShowcases();
 
-	deal->block(calcExpAltarForHero() == 0);
+	deal->block(calcExpAltarForHero() == 0 || !LOCPLINT->makingTurn);
 }
 
 void CAltarCreatures::updateAltarSlot(const std::shared_ptr<CTradeableItem> & slot)
@@ -222,7 +222,7 @@ void CAltarCreatures::onOfferSliderMoved(int newVal)
 		unitsOnAltar[bidTradePanel->highlightedSlot->serial] = newVal;
 	if(offerTradePanel->isHighlighted())
 		updateAltarSlot(offerTradePanel->highlightedSlot);
-	deal->block(calcExpAltarForHero() == 0);
+	deal->block(calcExpAltarForHero() == 0 || !LOCPLINT->makingTurn);
 	highlightingChanged();
 	redraw();
 }

+ 1 - 1
client/widgets/markets/CArtifactsBuying.cpp

@@ -102,7 +102,7 @@ void CArtifactsBuying::highlightingChanged()
 	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
 	{
 		market->getOffer(bidTradePanel->getSelectedItemId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::RESOURCE_ARTIFACT);
-		deal->block(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getSelectedItemId())) >= bidQty ? false : true);
+		deal->block(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getSelectedItemId())) < bidQty || !LOCPLINT->makingTurn);
 	}
 	CMarketBase::highlightingChanged();
 	CMarketTraderText::highlightingChanged();

+ 1 - 1
client/widgets/markets/CArtifactsSelling.cpp

@@ -149,7 +149,7 @@ void CArtifactsSelling::highlightingChanged()
 	if(art && offerTradePanel->isHighlighted())
 	{
 		market->getOffer(art->getTypeId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::ARTIFACT_RESOURCE);
-		deal->block(false);
+		deal->block(!LOCPLINT->makingTurn);
 	}
 	CMarketBase::highlightingChanged();
 	CMarketTraderText::highlightingChanged();

+ 1 - 1
client/widgets/markets/CFreelancerGuild.cpp

@@ -96,7 +96,7 @@ void CFreelancerGuild::highlightingChanged()
 		offerSlider->scrollTo(0);
 		offerSlider->block(false);
 		maxAmount->block(false);
-		deal->block(false);
+		deal->block(!LOCPLINT->makingTurn);
 	}
 	CMarketBase::highlightingChanged();
 	CMarketTraderText::highlightingChanged();

+ 1 - 1
client/widgets/markets/CMarketResources.cpp

@@ -89,7 +89,7 @@ void CMarketResources::highlightingChanged()
 		const bool isControlsBlocked = bidTradePanel->getSelectedItemId() != offerTradePanel->getSelectedItemId() ? false : true;
 		offerSlider->block(isControlsBlocked);
 		maxAmount->block(isControlsBlocked);
-		deal->block(isControlsBlocked);
+		deal->block(isControlsBlocked || !LOCPLINT->makingTurn);
 	}
 	CMarketBase::highlightingChanged();
 	CMarketTraderText::highlightingChanged();

+ 1 - 1
client/widgets/markets/CTransferResources.cpp

@@ -89,7 +89,7 @@ void CTransferResources::highlightingChanged()
 		offerSlider->scrollTo(0);
 		offerSlider->block(false);
 		maxAmount->block(false);
-		deal->block(false);
+		deal->block(!LOCPLINT->makingTurn);
 	}
 	CMarketBase::highlightingChanged();
 	CMarketTraderText::highlightingChanged();

+ 10 - 5
client/windows/CCastleInterface.cpp

@@ -625,7 +625,7 @@ void CCastleBuildings::recreate()
 		buildings.push_back(std::make_shared<CBuildingRect>(this, town, toAdd));
 	}
 
-	auto const & buildSorter = [] (const CIntObject * a, const CIntObject * b)
+	const auto & buildSorter = [](const CIntObject * a, const CIntObject * b)
 	{
 		auto b1 = dynamic_cast<const CBuildingRect *>(a);
 		auto b2 = dynamic_cast<const CBuildingRect *>(b);
@@ -783,11 +783,14 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 						if(upgrades == BuildingID::TAVERN)
 							LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE);
 						else
-						enterBuilding(building);
+							enterBuilding(building);
 						break;
 
 				case BuildingSubID::CASTLE_GATE:
-						enterCastleGate();
+						if (LOCPLINT->makingTurn)
+							enterCastleGate();
+						else
+							enterBuilding(building);
 						break;
 
 				case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer
@@ -966,7 +969,9 @@ void CCastleBuildings::enterMagesGuild()
 {
 	const CGHeroInstance *hero = getHero();
 
-	if(hero && !hero->hasSpellbook()) //hero doesn't have spellbok
+	// hero doesn't have spellbok
+	// or it is not our turn and we can't make actions
+	if(hero && !hero->hasSpellbook() && LOCPLINT->makingTurn)
 	{
 		if(hero->isCampaignYog())
 		{
@@ -1564,7 +1569,7 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin
 
 		buy = std::make_shared<CButton>(Point(45, 446), AnimationPath::builtin("IBUY30"), CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT);
 		buy->setBorderColor(Colors::METALLIC_GOLD);
-		buy->block(state!=EBuildingState::ALLOWED || LOCPLINT->playerID != town->tempOwner);
+		buy->block(state != EBuildingState::ALLOWED || LOCPLINT->playerID != town->tempOwner || !LOCPLINT->makingTurn);
 
 		cancel = std::make_shared<CButton>(Point(290, 445), AnimationPath::builtin("ICANCEL"), CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL);
 		cancel->setBorderColor(Colors::METALLIC_GOLD);

+ 1 - 1
client/windows/CSpellWindow.cpp

@@ -620,7 +620,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
 		const bool inCastle = owner->myInt->castleInt != nullptr;
 
 		//battle spell on adv map or adventure map spell during combat => display infowindow, not cast
-		if((combatSpell ^ inCombat) || inCastle)
+		if((combatSpell != inCombat) || inCastle || (!combatSpell && !LOCPLINT->makingTurn))
 		{
 			std::vector<std::shared_ptr<CComponent>> hlp(1, std::make_shared<CComponent>(ComponentType::SPELL, mySpell->id));
 			LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp);

+ 3 - 0
client/windows/CWindowWithArtifacts.cpp

@@ -89,6 +89,9 @@ void CWindowWithArtifacts::clickPressedArtPlaceHero(const CArtifactsOfHeroBase &
 	if(artPlace.isLocked())
 		return;
 
+	if (!LOCPLINT->makingTurn)
+		return;
+
 	std::visit(
 		[this, &artPlace](auto artSetWeak) -> void
 		{

+ 6 - 2
client/windows/GUIClasses.cpp

@@ -302,7 +302,7 @@ void CRecruitmentWindow::sliderMoved(int to)
 	if(!selected)
 		return;
 
-	buyButton->block(!to);
+	buyButton->block(!to || !LOCPLINT->makingTurn);
 	availableValue->setText(std::to_string(selected->amount - to));
 	toRecruitValue->setText(std::to_string(to));
 
@@ -479,7 +479,11 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func
 	recruit = std::make_shared<CButton>(Point(272, 355), AnimationPath::builtin("TPTAV01.DEF"), CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), EShortcut::GLOBAL_ACCEPT);
 	thiefGuild = std::make_shared<CButton>(Point(22, 428), AnimationPath::builtin("TPTAV02.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), EShortcut::ADVENTURE_THIEVES_GUILD);
 
-	if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold
+	if(!LOCPLINT->makingTurn)
+	{
+		recruit->block(true);
+	}
+	else if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold
 	{
 		recruit->addHoverText(EButtonState::NORMAL, CGI->generaltexth->tavernInfo[0]); //Cannot afford a Hero
 		recruit->block(true);

+ 6 - 0
server/CGameHandler.cpp

@@ -1748,6 +1748,12 @@ void CGameHandler::throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id)
 	}
 }
 
+void CGameHandler::throwIfPlayerNotActive(CPackForServer * pack)
+{
+	if (!turnOrder->isPlayerMakingTurn(pack->player))
+		throwNotAllowedAction(pack);
+}
+
 void CGameHandler::throwIfWrongPlayer(CPackForServer * pack)
 {
 	throwIfWrongPlayer(pack, pack->player);

+ 41 - 0
server/NetPacksServer.cpp

@@ -47,18 +47,21 @@ void ApplyGhNetPackVisitor::visitGamePause(GamePause & pack)
 void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
 	result = gh.turnOrder->onPlayerEndsTurn(pack.player);
 }
 
 void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.hid);
+	gh.throwIfPlayerNotActive(&pack);
 	result = gh.removeObject(gh.getObj(pack.hid), pack.player);
 }
 
 void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.hid);
+	gh.throwIfPlayerNotActive(&pack);
 
 	for (auto const & dest : pack.path)
 	{
@@ -75,6 +78,7 @@ void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack)
 void ApplyGhNetPackVisitor::visitCastleTeleportHero(CastleTeleportHero & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.hid);
+	gh.throwIfPlayerNotActive(&pack);
 
 	result = gh.teleportHero(pack.hid, pack.dest, pack.source, pack.player);
 }
@@ -82,48 +86,63 @@ void ApplyGhNetPackVisitor::visitCastleTeleportHero(CastleTeleportHero & pack)
 void ApplyGhNetPackVisitor::visitArrangeStacks(ArrangeStacks & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.arrangeStacks(pack.id1, pack.id2, pack.what, pack.p1, pack.p2, pack.val, pack.player);
 }
 
 void ApplyGhNetPackVisitor::visitBulkMoveArmy(BulkMoveArmy & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.srcArmy);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.bulkMoveArmy(pack.srcArmy, pack.destArmy, pack.srcSlot);
 }
 
 void ApplyGhNetPackVisitor::visitBulkSplitStack(BulkSplitStack & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.bulkSplitStack(pack.src, pack.srcOwner, pack.amount);
 }
 
 void ApplyGhNetPackVisitor::visitBulkMergeStacks(BulkMergeStacks & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.bulkMergeStacks(pack.src, pack.srcOwner);
 }
 
 void ApplyGhNetPackVisitor::visitBulkSmartSplitStack(BulkSmartSplitStack & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.bulkSmartSplitStack(pack.src, pack.srcOwner);
 }
 
 void ApplyGhNetPackVisitor::visitDisbandCreature(DisbandCreature & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.id);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.disbandCreature(pack.id, pack.pos);
 }
 
 void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.tid);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.buildStructure(pack.tid, pack.bid);
 }
 
 void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
 	// ownership checks are inside recruitCreatures
 	result = gh.recruitCreatures(pack.tid, pack.dst, pack.crid, pack.amount, pack.level, pack.player);
 }
@@ -131,6 +150,8 @@ void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack)
 void ApplyGhNetPackVisitor::visitUpgradeCreature(UpgradeCreature & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.id);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.upgradeCreature(pack.id, pack.pos, pack.cid);
 }
 
@@ -139,6 +160,8 @@ void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack)
 	const CGTownInstance * town = gh.getTown(pack.tid);
 	if(!gh.isPlayerOwns(&pack, pack.tid) && !(town->garrisonHero && gh.isPlayerOwns(&pack, town->garrisonHero->id)))
 		gh.throwNotAllowedAction(&pack); //neither town nor garrisoned hero (if present) is ours
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.garrisonSwap(pack.tid);
 }
 
@@ -146,6 +169,8 @@ void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack)
 {
 	if(gh.getHero(pack.src.artHolder))
 		gh.throwIfWrongPlayer(&pack, gh.getOwner(pack.src.artHolder)); //second hero can be ally
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.moveArtifact(pack.player, pack.src, pack.dst);
 }
 
@@ -155,11 +180,15 @@ void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & p
 		gh.throwIfWrongOwner(&pack, pack.srcHero);
 	if(pack.swap)
 		gh.throwIfWrongOwner(&pack, pack.dstHero);
+
+	gh.throwIfPlayerNotActive(&pack);
 	result = gh.bulkMoveArtifacts(pack.player, pack.srcHero, pack.dstHero, pack.swap, pack.equipped, pack.backpack);
 }
 
 void ApplyGhNetPackVisitor::visitManageBackpackArtifacts(ManageBackpackArtifacts & pack)
 {
+	gh.throwIfPlayerNotActive(&pack);
+
 	if(gh.getPlayerRelations(pack.player, gh.getOwner(pack.artHolder)) != PlayerRelations::ENEMIES)
 	{
 		if(pack.cmd == ManageBackpackArtifacts::ManageCmd::SCROLL_LEFT)
@@ -191,18 +220,21 @@ void ApplyGhNetPackVisitor::visitManageEquippedArtifacts(ManageEquippedArtifacts
 void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.heroID);
+	// gh.throwIfPlayerNotActive(&pack); // Might happen when player captures artifacts in battle?
 	result = gh.assembleArtifacts(pack.heroID, pack.artifactSlot, pack.assemble, pack.assembleTo);
 }
 
 void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack)
 {
 	gh.throwIfWrongPlayer(&pack, gh.getOwner(pack.al.artHolder));
+	gh.throwIfPlayerNotActive(&pack);
 	result = gh.eraseArtifactByClient(pack.al);
 }
 
 void ApplyGhNetPackVisitor::visitBuyArtifact(BuyArtifact & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.hid);
+	gh.throwIfPlayerNotActive(&pack);
 	result = gh.buyArtifact(pack.hid, pack.aid);
 }
 
@@ -213,6 +245,7 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack)
 	const auto * market = dynamic_cast<const IMarket*>(object);
 
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
 
 	if(!object)
 		gh.throwAndComplain(&pack, "Invalid market object");
@@ -317,12 +350,15 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack)
 void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.hid);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.setFormation(pack.hid, pack.formation);
 }
 
 void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
 
 	result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player, pack.nhid);
 }
@@ -330,6 +366,7 @@ void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack)
 void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	gh.throwIfPlayerNotActive(&pack);
 
 	if(gh.getPlayerRelations(gh.getOwner(pack.objid), pack.player) == PlayerRelations::ENEMIES)
 		gh.throwAndComplain(&pack, "Can't build boat at enemy shipyard");
@@ -355,6 +392,7 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack)
 void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
+	// allowed even if it is not our turn - will be filtered by battle sides
 
 	result = gh.battles->makePlayerBattleAction(pack.battleID, pack.player, pack.ba);
 }
@@ -362,12 +400,15 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack)
 void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.id);
+	gh.throwIfPlayerNotActive(&pack);
+
 	result = gh.dig(gh.getHero(pack.id));
 }
 
 void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack)
 {
 	gh.throwIfWrongOwner(&pack, pack.hid);
+	gh.throwIfPlayerNotActive(&pack);
 
 	if (!pack.sid.hasValue())
 		gh.throwNotAllowedAction(&pack);

+ 1 - 1
server/processors/TurnOrderProcessor.h

@@ -71,13 +71,13 @@ class TurnOrderProcessor : boost::noncopyable
 	void doEndPlayerTurn(PlayerColor which);
 
 	bool isPlayerAwaitsTurn(PlayerColor which) const;
-	bool isPlayerMakingTurn(PlayerColor which) const;
 	bool isPlayerAwaitsNewDay(PlayerColor which) const;
 
 public:
 	TurnOrderProcessor(CGameHandler * owner);
 
 	bool isContactAllowed(PlayerColor left, PlayerColor right) const;
+	bool isPlayerMakingTurn(PlayerColor which) const;
 
 	/// Add new player to handle (e.g. on game start)
 	void addPlayer(PlayerColor which);