2
0
Эх сурвалжийг харах

Merge branch 'develop' into autosave

Michael 2 жил өмнө
parent
commit
fed9ae5157

BIN
Mods/vcmi/Sounds/we5.wav


+ 4 - 0
Mods/vcmi/mod.json

@@ -190,6 +190,10 @@
 		"MAPS/":
 		[
 			{"type" : "dir",  "path" : "/Maps"}
+		],
+		"SOUNDS/":
+		[
+			{"type" : "dir", "path" : "/Sounds"}
 		]
 	}
 }

+ 2 - 0
client/CMakeLists.txt

@@ -12,6 +12,7 @@ set(client_SRCS
 	adventureMap/CMinimap.cpp
 	adventureMap/CResDataBar.cpp
 	adventureMap/MapAudioPlayer.cpp
+	adventureMap/TurnTimerWidget.cpp
 
 	battle/BattleActionsController.cpp
 	battle/BattleAnimationClasses.cpp
@@ -157,6 +158,7 @@ set(client_HEADERS
 	adventureMap/CMinimap.h
 	adventureMap/CResDataBar.h
 	adventureMap/MapAudioPlayer.h
+	adventureMap/TurnTimerWidget.h
 
 	battle/BattleActionsController.h
 	battle/BattleAnimationClasses.h

+ 8 - 0
client/CPlayerInterface.cpp

@@ -169,6 +169,9 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
 void CPlayerInterface::playerStartsTurn(PlayerColor player)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
+	
+	makingTurn = false;
+	stillMoveHero.setn(STOP_MOVE);
 
 	if(GH.windows().findWindows<AdventureMapInterface>().empty())
 	{
@@ -177,6 +180,11 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player)
 		GH.windows().pushWindow(adventureInt);
 	}
 
+	//close window from another player
+	if(auto w = GH.windows().topWindow<CInfoWindow>())
+		if(w->ID == -1 && player != playerID)
+			w->close();
+	
 	// remove all dialogs that do not expect query answer
 	while (!GH.windows().topWindow<AdventureMapInterface>() && !GH.windows().topWindow<CInfoWindow>())
 		GH.windows().popWindows(1);

+ 1 - 1
client/CServerHandler.cpp

@@ -476,7 +476,7 @@ void CServerHandler::setTurnLength(int npos) const
 {
 	vstd::amin(npos, GameConstants::POSSIBLE_TURNTIME.size() - 1);
 	LobbySetTurnTime lstt;
-	lstt.turnTime = GameConstants::POSSIBLE_TURNTIME[npos];
+	lstt.turnTimerInfo.turnTimer = GameConstants::POSSIBLE_TURNTIME[npos] * 60 * 1000;
 	sendLobbyPack(lstt);
 }
 

+ 1 - 0
client/ClientNetPackVisitors.h

@@ -94,6 +94,7 @@ public:
 	void visitSystemMessage(SystemMessage & pack) override;
 	void visitPlayerBlocked(PlayerBlocked & pack) override;
 	void visitYourTurn(YourTurn & pack) override;
+	void visitTurnTimeUpdate(TurnTimeUpdate & pack) override;
 	void visitPlayerMessageClient(PlayerMessageClient & pack) override;
 	void visitAdvmapSpellCast(AdvmapSpellCast & pack) override;
 	void visitShowWorldViewEx(ShowWorldViewEx & pack) override;	

+ 5 - 0
client/NetPacksClient.cpp

@@ -864,6 +864,11 @@ void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack)
 	callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn);
 }
 
+void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack)
+{
+	logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.getStr());
+}
+
 void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack)
 {
 	logNetwork->debug("pack.player %s sends a message: %s", pack.player.getStr(), pack.text);

+ 5 - 0
client/adventureMap/AdventureMapInterface.cpp

@@ -17,6 +17,7 @@
 #include "CList.h"
 #include "CInfoBar.h"
 #include "MapAudioPlayer.h"
+#include "TurnTimerWidget.h"
 #include "AdventureMapWidget.h"
 #include "AdventureMapShortcuts.h"
 
@@ -35,6 +36,7 @@
 
 #include "../../CCallback.h"
 #include "../../lib/CConfigHandler.h"
+#include "../../lib/StartInfo.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
@@ -61,6 +63,9 @@ AdventureMapInterface::AdventureMapInterface():
 	shortcuts->setState(EAdventureState::MAKING_TURN);
 	widget->getMapView()->onViewMapActivated();
 
+	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled())
+		watches = std::make_shared<TurnTimerWidget>();
+	
 	addUsedEvents(KEYBOARD | TIME);
 }
 

+ 2 - 0
client/adventureMap/AdventureMapInterface.h

@@ -39,6 +39,7 @@ class CTownList;
 class CInfoBar;
 class CMinimap;
 class MapAudioPlayer;
+class TurnTimerWidget;
 enum class EAdventureState;
 
 struct MapDrawingInfo;
@@ -64,6 +65,7 @@ private:
 	std::shared_ptr<MapAudioPlayer> mapAudio;
 	std::shared_ptr<AdventureMapWidget> widget;
 	std::shared_ptr<AdventureMapShortcuts> shortcuts;
+	std::shared_ptr<TurnTimerWidget> watches;
 
 private:
 	void setState(EAdventureState state);

+ 1 - 1
client/adventureMap/AdventureMapShortcuts.cpp

@@ -314,7 +314,7 @@ void AdventureMapShortcuts::visitObject()
 	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
 
 	if(h)
-		LOCPLINT->cb->moveHero(h,h->pos);
+		LOCPLINT->cb->moveHero(h, h->pos);
 }
 
 void AdventureMapShortcuts::openObject()

+ 129 - 0
client/adventureMap/TurnTimerWidget.cpp

@@ -0,0 +1,129 @@
+/*
+ * TurnTimerWidget.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "TurnTimerWidget.h"
+
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+
+#include "../render/EFont.h"
+#include "../render/Graphics.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/TextAlignment.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+#include "../../CCallback.h"
+#include "../../lib/CPlayerState.h"
+#include "../../lib/filesystem/ResourceID.h"
+
+TurnTimerWidget::DrawRect::DrawRect(const Rect & r, const ColorRGBA & c):
+	CIntObject(), rect(r), color(c)
+{
+}
+
+void TurnTimerWidget::DrawRect::showAll(Canvas & to)
+{
+	to.drawColor(rect, color);
+	
+	CIntObject::showAll(to);
+}
+
+TurnTimerWidget::TurnTimerWidget():
+	InterfaceObjectConfigurable(TIME),
+	turnTime(0), lastTurnTime(0), cachedTurnTime(0)
+{
+	REGISTER_BUILDER("drawRect", &TurnTimerWidget::buildDrawRect);
+	
+	recActions &= ~DEACTIVATE;
+	
+	const JsonNode config(ResourceID("config/widgets/turnTimer.json"));
+	
+	build(config);
+	
+	std::transform(variables["notificationTime"].Vector().begin(),
+				   variables["notificationTime"].Vector().end(),
+				   std::inserter(notifications, notifications.begin()),
+				   [](const JsonNode & node){ return node.Integer(); });
+}
+
+std::shared_ptr<TurnTimerWidget::DrawRect> TurnTimerWidget::buildDrawRect(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget TurnTimerWidget::DrawRect");
+	auto rect = readRect(config["rect"]);
+	auto color = readColor(config["color"]);
+	return std::make_shared<TurnTimerWidget::DrawRect>(rect, color);
+}
+
+void TurnTimerWidget::show(Canvas & to)
+{
+	showAll(to);
+}
+
+void TurnTimerWidget::setTime(int time)
+{
+	int newTime = time / 1000;
+	if((LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID)
+	   && (newTime != turnTime)
+	   && notifications.count(newTime))
+		CCS->soundh->playSound(variables["notificationSound"].String());
+	turnTime = newTime;
+	if(auto w = widget<CLabel>("timer"))
+	{
+		std::ostringstream oss;
+		oss << turnTime / 60 << ":" << std::setw(2) << std::setfill('0') << turnTime % 60;
+		w->setText(oss.str());
+		
+		if(graphics && LOCPLINT && LOCPLINT->cb
+		   && variables["textColorFromPlayerColor"].Bool()
+		   && LOCPLINT->cb->getCurrentPlayer().isValidPlayer())
+		{
+			w->setColor(graphics->playerColors[LOCPLINT->cb->getCurrentPlayer()]);
+		}
+	}
+}
+
+void TurnTimerWidget::tick(uint32_t msPassed)
+{
+	if(LOCPLINT && LOCPLINT->cb)
+	{
+		auto player = LOCPLINT->cb->getCurrentPlayer();
+		auto time = LOCPLINT->cb->getPlayerTurnTime(player);
+		cachedTurnTime -= msPassed;
+		if(cachedTurnTime < 0) cachedTurnTime = 0; //do not go below zero
+		
+		auto timeCheckAndUpdate = [&](int time)
+		{
+			if(time / 1000 != lastTurnTime / 1000)
+			{
+				//do not update timer on this tick
+				lastTurnTime = time;
+				cachedTurnTime = time;
+			}
+			else setTime(cachedTurnTime);
+		};
+		
+		auto * playerInfo = LOCPLINT->cb->getPlayer(player);
+		if(playerInfo && playerInfo->isHuman())
+		{
+			if(LOCPLINT->battleInt)
+			{
+				if(time.isBattleEnabled())
+					timeCheckAndUpdate(time.creatureTimer);
+			}
+			else
+			{
+				timeCheckAndUpdate(time.turnTimer);
+			}
+		}
+		else
+			timeCheckAndUpdate(0);
+	}
+}

+ 51 - 0
client/adventureMap/TurnTimerWidget.h

@@ -0,0 +1,51 @@
+/*
+ * TurnTimerWidget.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+#include "../gui/CIntObject.h"
+#include "../gui/InterfaceObjectConfigurable.h"
+#include "../render/Canvas.h"
+#include "../render/Colors.h"
+
+class CAnimImage;
+class CLabel;
+
+class TurnTimerWidget : public InterfaceObjectConfigurable
+{
+private:
+	
+	class DrawRect : public CIntObject
+	{
+		const Rect rect;
+		const ColorRGBA color;
+		
+	public:
+		DrawRect(const Rect &, const ColorRGBA &);
+		void showAll(Canvas & to) override;
+	};
+
+	int turnTime;
+	int lastTurnTime;
+	int cachedTurnTime;
+	
+	std::set<int> notifications;
+	
+	std::shared_ptr<DrawRect> buildDrawRect(const JsonNode & config) const;
+	
+public:
+
+	void show(Canvas & to) override;
+	void tick(uint32_t msPassed) override;
+	
+	void setTime(int time);
+
+	TurnTimerWidget();
+};

+ 23 - 12
client/gui/InterfaceObjectConfigurable.cpp

@@ -190,18 +190,29 @@ ColorRGBA InterfaceObjectConfigurable::readColor(const JsonNode & config) const
 	logGlobal->debug("Reading color");
 	if(!config.isNull())
 	{
-		if(config.String() == "yellow")
-			return Colors::YELLOW;
-		if(config.String() == "white")
-			return Colors::WHITE;
-		if(config.String() == "gold")
-			return Colors::METALLIC_GOLD;
-		if(config.String() == "green")
-			return Colors::GREEN;
-		if(config.String() == "orange")
-			return Colors::ORANGE;
-		if(config.String() == "bright-yellow")
-			return Colors::BRIGHT_YELLOW;
+		if(config.isString())
+		{
+			if(config.String() == "yellow")
+				return Colors::YELLOW;
+			if(config.String() == "white")
+				return Colors::WHITE;
+			if(config.String() == "gold")
+				return Colors::METALLIC_GOLD;
+			if(config.String() == "green")
+				return Colors::GREEN;
+			if(config.String() == "orange")
+				return Colors::ORANGE;
+			if(config.String() == "bright-yellow")
+				return Colors::BRIGHT_YELLOW;
+		}
+		if(config.isVector())
+		{
+			const auto & asVector = config.Vector();
+			if(asVector.size() == 4)
+				return ColorRGBA(asVector[0].Integer(), asVector[1].Integer(), asVector[2].Integer(), asVector[3].Integer());
+			if(asVector.size() == 3)
+				return ColorRGBA(asVector[0].Integer(), asVector[1].Integer(), asVector[2].Integer());
+		}
 	}
 	logGlobal->debug("Uknown color attribute");
 	return Colors::DEFAULT_KEY_COLOR;

+ 1 - 1
client/lobby/OptionsTab.cpp

@@ -78,7 +78,7 @@ void OptionsTab::recreate()
 
 	if(sliderTurnDuration)
 	{
-		sliderTurnDuration->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTime));
+		sliderTurnDuration->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTimerInfo.turnTimer / (60 * 1000)));
 		labelTurnDurationValue->setText(CGI->generaltexth->turnDurations[sliderTurnDuration->getValue()]);
 	}
 }

+ 2 - 0
cmake_modules/VCMI_lib.cmake

@@ -265,6 +265,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/ScriptHandler.cpp
 		${MAIN_LIB_DIR}/TerrainHandler.cpp
 		${MAIN_LIB_DIR}/TextOperations.cpp
+		${MAIN_LIB_DIR}/TurnTimerInfo.cpp
 		${MAIN_LIB_DIR}/VCMIDirs.cpp
 		${MAIN_LIB_DIR}/VCMI_Lib.cpp
 	)
@@ -623,6 +624,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/StringConstants.h
 		${MAIN_LIB_DIR}/TerrainHandler.h
 		${MAIN_LIB_DIR}/TextOperations.h
+		${MAIN_LIB_DIR}/TurnTimerInfo.h
 		${MAIN_LIB_DIR}/UnlockGuard.h
 		${MAIN_LIB_DIR}/VCMIDirs.h
 		${MAIN_LIB_DIR}/vcmi_endian.h

+ 34 - 0
config/widgets/turnTimer.json

@@ -0,0 +1,34 @@
+{
+	"items":
+	[
+		{ //backgound color
+			"type": "drawRect",
+			"rect": {"x": 4, "y": 4, "w": 68, "h": 24},
+			"color": [10, 10, 10, 255]
+		},
+
+		{ //clocks icon
+			"type": "image",
+			"image": "VCMI/BATTLEQUEUE/STATESSMALL",
+			"frame": 1,
+			"position": {"x": 4, "y": 6}
+		},
+
+		{ //timer field label
+			"name": "timer",
+			"type": "label",
+			"font": "big",
+			"alignment": "left",
+			"color": "yellow",
+			"text": "",
+			"position": {"x": 24, "y": 2}
+		},
+	],
+
+	"variables":
+	{
+		"notificationTime": [0, 1, 2, 3, 4, 5, 20],
+		"notificationSound": "WE5",
+		"textColorFromPlayerColor": true
+	}
+}

+ 16 - 0
lib/CGameInfoCallback.cpp

@@ -99,6 +99,22 @@ const PlayerState * CGameInfoCallback::getPlayerState(PlayerColor color, bool ve
 	}
 }
 
+TurnTimerInfo CGameInfoCallback::getPlayerTurnTime(PlayerColor color) const
+{
+	if(!color.isValidPlayer())
+	{
+		return TurnTimerInfo{};
+	}
+	
+	auto player = gs->players.find(color);
+	if(player != gs->players.end())
+	{
+		return player->second.turnTimer;
+	}
+	
+	return TurnTimerInfo{};
+}
+
 const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(int identifier) const
 {
 	if(gs->map->questIdentifierToId.empty())

+ 2 - 0
lib/CGameInfoCallback.h

@@ -37,6 +37,7 @@ struct TeamState;
 struct QuestInfo;
 class CGameState;
 class PathfinderConfig;
+struct TurnTimerInfo;
 
 class CArmedInstance;
 class CGObjectInstance;
@@ -153,6 +154,7 @@ public:
 	virtual PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns
 	PlayerColor getLocalPlayer() const override; //player that is currently owning given client (if not a client, then returns current player)
 	virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const;
+	virtual TurnTimerInfo getPlayerTurnTime(PlayerColor color) const;
 
 	//map
 	virtual bool isVisible(int3 pos, const std::optional<PlayerColor> & Player) const;

+ 3 - 0
lib/CPlayerState.h

@@ -15,6 +15,7 @@
 #include "bonuses/Bonus.h"
 #include "bonuses/CBonusSystemNode.h"
 #include "ResourceSet.h"
+#include "TurnTimerInfo.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -39,6 +40,7 @@ public:
 	bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory
 	EPlayerStatus::EStatus status;
 	std::optional<ui8> daysWithoutCastle;
+	TurnTimerInfo turnTimer;
 
 	PlayerState();
 	PlayerState(PlayerState && other) noexcept;
@@ -71,6 +73,7 @@ public:
 		h & team;
 		h & resources;
 		h & status;
+		h & turnTimer;
 		h & heroes;
 		h & towns;
 		h & dwellings;

+ 1 - 0
lib/NetPackVisitor.h

@@ -27,6 +27,7 @@ public:
 	virtual void visitPlayerBlocked(PlayerBlocked & pack) {}
 	virtual void visitPlayerCheated(PlayerCheated & pack) {}
 	virtual void visitYourTurn(YourTurn & pack) {}
+	virtual void visitTurnTimeUpdate(TurnTimeUpdate & pack) {}
 	virtual void visitEntitiesChanged(EntitiesChanged & pack) {}
 	virtual void visitSetResources(SetResources & pack) {}
 	virtual void visitSetPrimSkill(SetPrimSkill & pack) {}

+ 15 - 0
lib/NetPacks.h

@@ -14,6 +14,7 @@
 #include "ConstTransitivePtr.h"
 #include "MetaString.h"
 #include "ResourceSet.h"
+#include "TurnTimerInfo.h"
 #include "int3.h"
 
 #include "battle/BattleAction.h"
@@ -147,6 +148,20 @@ struct DLL_LINKAGE PlayerCheated : public CPackForClient
 	}
 };
 
+struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient
+{
+	void applyGs(CGameState * gs) const;
+	
+	PlayerColor player;
+	TurnTimerInfo turnTimer;
+		
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & player;
+		h & turnTimer;
+	}
+};
+
 struct DLL_LINKAGE YourTurn : public CPackForClient
 {
 	void applyGs(CGameState * gs) const;

+ 6 - 0
lib/NetPacksLib.cpp

@@ -2509,6 +2509,12 @@ void YourTurn::applyGs(CGameState * gs) const
 	playerState.daysWithoutCastle = daysWithoutCastle;
 }
 
+void TurnTimeUpdate::applyGs(CGameState *gs) const
+{
+	auto & playerState = gs->players[player];
+	playerState.turnTimer = turnTimer;
+}
+
 Component::Component(const CStackBasicDescriptor & stack)
 	: id(EComponentType::CREATURE)
 	, subtype(stack.type->getId())

+ 2 - 2
lib/NetPacksLobby.h

@@ -240,13 +240,13 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer
 
 struct DLL_LINKAGE LobbySetTurnTime : public CLobbyPackToServer
 {
-	ui8 turnTime = 0;
+	TurnTimerInfo turnTimerInfo;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & turnTime;
+		h & turnTimerInfo;
 	}
 };
 

+ 6 - 5
lib/StartInfo.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "GameConstants.h"
+#include "TurnTimerInfo.h"
 #include "campaign/CampaignConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -80,8 +81,8 @@ struct DLL_LINKAGE StartInfo
 	ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack)
 	ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet
 	ui32 mapfileChecksum; //0 if not relevant
-	std::string gameUuid;
-	ui8 turnTime; //in minutes, 0=unlimited
+  std::string gameUuid;
+	TurnTimerInfo turnTimerInfo;
 	std::string mapname; // empty for random map, otherwise name of the map or savegame
 	bool createRandomMap() const { return mapGenOptions != nullptr; }
 	std::shared_ptr<CMapGenOptions> mapGenOptions;
@@ -104,15 +105,15 @@ struct DLL_LINKAGE StartInfo
 		h & seedToBeUsed;
 		h & seedPostInit;
 		h & mapfileChecksum;
-		h & gameUuid;
-		h & turnTime;
+  	h & gameUuid;
+		h & turnTimerInfo;
 		h & mapname;
 		h & mapGenOptions;
 		h & campState;
 	}
 
 	StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0),
-		mapfileChecksum(0), turnTime(0), uuid(boost::uuids::to_string(boost::uuids::random_generator()()))
+		mapfileChecksum(0), uuid(boost::uuids::to_string(boost::uuids::random_generator()()))
 	{
 
 	}

+ 25 - 0
lib/TurnTimerInfo.cpp

@@ -0,0 +1,25 @@
+/*
+ * TurnTimerInfo.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "TurnTimerInfo.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+bool TurnTimerInfo::isEnabled() const
+{
+	return turnTimer > 0;
+}
+
+bool TurnTimerInfo::isBattleEnabled() const
+{
+	return creatureTimer > 0;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 35 - 0
lib/TurnTimerInfo.h

@@ -0,0 +1,35 @@
+/*
+ * TurnTimerInfo.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct DLL_LINKAGE TurnTimerInfo
+{
+	int turnTimer = 0; //in ms, counts down when player is making his turn on adventure map
+	int baseTimer = 0; //in ms, counts down only when turn timer runs out
+	int battleTimer = 0; //in ms, counts down during battles when creature timer runs out
+	int creatureTimer = 0; //in ms, counts down when player is choosing action in battle
+	
+	bool isEnabled() const;
+	bool isBattleEnabled() const;
+	
+	template <typename Handler>
+	void serialize(Handler &h, const int version)
+	{
+		h & turnTimer;
+		h & baseTimer;
+		h & battleTimer;
+		h & creatureTimer;
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 1 - 0
lib/registerTypes/RegisterTypes.h

@@ -232,6 +232,7 @@ void registerTypesClientPacks1(Serializer &s)
 	s.template registerType<CPackForClient, PlayerBlocked>();
 	s.template registerType<CPackForClient, PlayerCheated>();
 	s.template registerType<CPackForClient, YourTurn>();
+	s.template registerType<CPackForClient, TurnTimeUpdate>();
 	s.template registerType<CPackForClient, SetResources>();
 	s.template registerType<CPackForClient, SetPrimSkill>();
 	s.template registerType<CPackForClient, SetSecSkill>();

+ 33 - 3
server/CGameHandler.cpp

@@ -541,6 +541,11 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack)
 	vstd::clear_pointer(pack);
 }
 
+
+CGameHandler::CGameHandler()
+	: turnTimerHandler(*this)
+{}
+
 CGameHandler::CGameHandler(CVCMIServer * lobby)
 	: lobby(lobby)
 	, heroPool(std::make_unique<HeroPoolProcessor>(this))
@@ -550,6 +555,7 @@ CGameHandler::CGameHandler(CVCMIServer * lobby)
 	, complainNoCreatures("No creatures to split")
 	, complainNotEnoughCreatures("Cannot split that stack, not enough creatures!")
 	, complainInvalidSlot("Invalid slot accessed!")
+	, turnTimerHandler(*this)
 {
 	QID = 1;
 	IObjectInterface::cb = this;
@@ -992,7 +998,11 @@ void CGameHandler::run(bool resume)
 		events::GameResumed::defaultExecute(serverEventBus.get());
 
 	auto playerTurnOrder = generatePlayerTurnOrder();
-
+	
+	if(!resume)
+		for(auto & playerColor : playerTurnOrder)
+			turnTimerHandler.onGameplayStart(gs->players[playerColor]);
+	
 	while(lobby->state == EServerState::GAMEPLAY)
 	{
 		if(!resume)
@@ -1040,6 +1050,8 @@ void CGameHandler::run(bool resume)
 					//Change local daysWithoutCastle counter for local interface message //TODO: needed?
 					yt.daysWithoutCastle = playerState->daysWithoutCastle;
 					applyAndSend(&yt);
+					
+					turnTimerHandler.onPlayerGetTurn(gs->players[player]);
 				}
 			};
 
@@ -1048,10 +1060,15 @@ void CGameHandler::run(bool resume)
 			if(playerColor != PlayerColor::CANNOT_DETERMINE)
 			{
 				//wait till turn is done
+				const int waitTime = 100; //ms
 				boost::unique_lock<boost::mutex> lock(states.mx);
 				while(states.players.at(playerColor).makingTurn && lobby->state == EServerState::GAMEPLAY)
 				{
-					static time_duration p = milliseconds(100);
+					turnTimerHandler.onPlayerMakingTurn(gs->players[playerColor], waitTime);
+					if(gs->curB)
+						turnTimerHandler.onBattleLoop(waitTime);
+
+					static time_duration p = milliseconds(waitTime);
 					states.cv.timed_wait(lock, p);
 				}
 			}
@@ -1141,6 +1158,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 	// not turn of that hero or player can't simply teleport hero (at least not with this function)
 	if (!h  || (asker != PlayerColor::NEUTRAL && (teleporting || h->getOwner() != gs->currentPlayer)))
 	{
+		if(h && getStartInfo()->turnTimerInfo.isEnabled() && gs->players[h->getOwner()].turnTimer.turnTimer == 0)
+			return true; //timer expired, no error
+		
 		logGlobal->error("Illegal call to move hero!");
 		return false;
 	}
@@ -1248,7 +1268,17 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 			visitObjectOnTile(t, h);
 		}
 
-		queries->popIfTop(moveQuery);
+		if(!transit)
+		{
+			for(auto topQuery = queries->topQuery(h->tempOwner); true; topQuery = queries->topQuery(h->tempOwner))
+			{
+				moveQuery = std::dynamic_pointer_cast<CHeroMovementQuery>(topQuery);
+				if(moveQuery)
+					queries->popIfTop(moveQuery);
+				else
+					break;
+			}
+		}
 		logGlobal->trace("Hero %s ends movement", h->getNameTranslated());
 		return result != TryMoveHero::FAILED;
 	};

+ 4 - 1
server/CGameHandler.h

@@ -14,6 +14,7 @@
 #include "../lib/IGameCallback.h"
 #include "../lib/battle/CBattleInfoCallback.h"
 #include "../lib/ScriptHandler.h"
+#include "TurnTimerHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -105,6 +106,8 @@ public:
 
 
 	SpellCastEnvironment * spellEnv;
+	
+	TurnTimerHandler turnTimerHandler;
 
 	const Services * services() const override;
 	const BattleCb * battle() const override;
@@ -118,7 +121,7 @@ public:
 	bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2);
 	void giveSpells(const CGTownInstance *t, const CGHeroInstance *h);
 
-	CGameHandler() = default;
+	CGameHandler();
 	CGameHandler(CVCMIServer * lobby);
 	~CGameHandler();
 

+ 2 - 0
server/CMakeLists.txt

@@ -19,6 +19,7 @@ set(server_SRCS
 		CVCMIServer.cpp
 		NetPacksServer.cpp
 		NetPacksLobbyServer.cpp
+		TurnTimerHandler.cpp
 )
 
 set(server_HEADERS
@@ -42,6 +43,7 @@ set(server_HEADERS
 		CVCMIServer.h
 		LobbyNetPackVisitors.h
 		ServerNetPackVisitors.h
+		TurnTimerHandler.h
 )
 
 assign_source_group(${server_SRCS} ${server_HEADERS})

+ 2 - 2
server/NetPacksLobbyServer.cpp

@@ -214,7 +214,7 @@ void ApplyOnServerNetPackVisitor::visitLobbySetCampaign(LobbySetCampaign & pack)
 	srv.si->mapname = pack.ourCampaign->getFilename();
 	srv.si->mode = StartInfo::CAMPAIGN;
 	srv.si->campState = pack.ourCampaign;
-	srv.si->turnTime = 0;
+	srv.si->turnTimerInfo = TurnTimerInfo{};
 
 	bool isCurrentMapConquerable = pack.ourCampaign->currentScenario() && pack.ourCampaign->isAvailable(*pack.ourCampaign->currentScenario());
 
@@ -391,7 +391,7 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayer(LobbySetPlayer & pack)
 
 void ApplyOnServerNetPackVisitor::visitLobbySetTurnTime(LobbySetTurnTime & pack)
 {
-	srv.si->turnTime = pack.turnTime;
+	srv.si->turnTimerInfo = pack.turnTimerInfo;
 	result = true;
 }
 

+ 0 - 1
server/NetPacksServer.cpp

@@ -64,7 +64,6 @@ void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack)
 
 void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack)
 {
-	gh.throwOnWrongOwner(&pack, pack.hid);
 	result = gh.moveHero(pack.hid, pack.dest, 0, pack.transit, gh.getPlayerAt(pack.c));
 }
 

+ 195 - 0
server/TurnTimerHandler.cpp

@@ -0,0 +1,195 @@
+/*
+ * TurnTimerHandler.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "TurnTimerHandler.h"
+#include "CGameHandler.h"
+#include "battles/BattleProcessor.h"
+#include "queries/QueriesProcessor.h"
+#include "../lib/battle/BattleInfo.h"
+#include "../lib/gameState/CGameState.h"
+#include "../lib/CPlayerState.h"
+#include "../lib/CStack.h"
+#include "../lib/StartInfo.h"
+#include "../lib/NetPacks.h"
+
+TurnTimerHandler::TurnTimerHandler(CGameHandler & gh):
+	gameHandler(gh)
+{
+	
+}
+
+void TurnTimerHandler::onGameplayStart(PlayerState & state)
+{
+	if(const auto * si = gameHandler.getStartInfo())
+	{
+		if(si->turnTimerInfo.isEnabled())
+		{
+			state.turnTimer = si->turnTimerInfo;
+			state.turnTimer.turnTimer = 0;
+		}
+	}
+}
+
+void TurnTimerHandler::onPlayerGetTurn(PlayerState & state)
+{
+	if(const auto * si = gameHandler.getStartInfo())
+	{
+		if(si->turnTimerInfo.isEnabled())
+		{
+			state.turnTimer.baseTimer += state.turnTimer.turnTimer;
+			state.turnTimer.turnTimer = si->turnTimerInfo.turnTimer;
+			
+			TurnTimeUpdate ttu;
+			ttu.player = state.color;
+			ttu.turnTimer = state.turnTimer;
+			gameHandler.sendAndApply(&ttu);
+		}
+	}
+}
+
+void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime)
+{
+	const auto * gs = gameHandler.gameState();
+	const auto * si = gameHandler.getStartInfo();
+	if(!si || !gs)
+		return;
+	
+	if(state.human && si->turnTimerInfo.isEnabled() && !gs->curB)
+	{
+		if(state.turnTimer.turnTimer > 0)
+		{
+			state.turnTimer.turnTimer -= waitTime;
+			int frequency = (state.turnTimer.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit);
+			
+			if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already
+			   && state.turnTimer.turnTimer % frequency == 0)
+			{
+				TurnTimeUpdate ttu;
+				ttu.player = state.color;
+				ttu.turnTimer = state.turnTimer;
+				gameHandler.sendAndApply(&ttu);
+			}
+		}
+		else if(state.turnTimer.baseTimer > 0)
+		{
+			state.turnTimer.turnTimer = state.turnTimer.baseTimer;
+			state.turnTimer.baseTimer = 0;
+			onPlayerMakingTurn(state, waitTime);
+		}
+		else if(!gameHandler.queries->topQuery(state.color)) //wait for replies to avoid pending queries
+			gameHandler.states.players.at(state.color).makingTurn = false; //force end turn
+	}
+}
+
+void TurnTimerHandler::onBattleStart()
+{
+	const auto * gs = gameHandler.gameState();
+	const auto * si = gameHandler.getStartInfo();
+	if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled())
+		return;
+	
+	auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER);
+	auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER);
+	
+	for(auto i : {attacker, defender})
+	{
+		if(i.isValidPlayer())
+		{
+			const auto & state = gs->players.at(i);
+			TurnTimeUpdate ttu;
+			ttu.player = state.color;
+			ttu.turnTimer = state.turnTimer;
+			ttu.turnTimer.battleTimer = si->turnTimerInfo.battleTimer;
+			ttu.turnTimer.creatureTimer = si->turnTimerInfo.creatureTimer;
+			gameHandler.sendAndApply(&ttu);
+		}
+	}
+}
+
+void TurnTimerHandler::onBattleNextStack(const CStack & stack)
+{
+	const auto * gs = gameHandler.gameState();
+	const auto * si = gameHandler.getStartInfo();
+	if(!si || !gs || !gs->curB)
+		return;
+	
+	if(!stack.getOwner().isValidPlayer())
+		return;
+	
+	const auto & state = gs->players.at(stack.getOwner());
+	
+	if(si->turnTimerInfo.isBattleEnabled())
+	{
+		TurnTimeUpdate ttu;
+		ttu.player = state.color;
+		ttu.turnTimer = state.turnTimer;
+		if(state.turnTimer.battleTimer < si->turnTimerInfo.battleTimer)
+			ttu.turnTimer.battleTimer = ttu.turnTimer.creatureTimer;
+		ttu.turnTimer.creatureTimer = si->turnTimerInfo.creatureTimer;
+		gameHandler.sendAndApply(&ttu);
+	}
+}
+
+void TurnTimerHandler::onBattleLoop(int waitTime)
+{
+	const auto * gs = gameHandler.gameState();
+	const auto * si = gameHandler.getStartInfo();
+	if(!si || !gs || !gs->curB)
+		return;
+	
+	const auto * stack = gs->curB.get()->battleGetStackByID(gs->curB->getActiveStackID());
+	if(!stack || !stack->getOwner().isValidPlayer())
+		return;
+	
+	auto & state = gs->players.at(gs->curB->getSidePlayer(stack->unitSide()));
+	
+	auto turnTimerUpdateApplier = [&](const TurnTimerInfo & tTimer)
+	{
+		TurnTimerInfo turnTimerUpdate = tTimer;
+		if(tTimer.creatureTimer > 0)
+		{
+			turnTimerUpdate.creatureTimer -= waitTime;
+			int frequency = (turnTimerUpdate.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit);
+			
+			if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already
+			   && turnTimerUpdate.creatureTimer % frequency == 0)
+			{
+				TurnTimeUpdate ttu;
+				ttu.player = state.color;
+				ttu.turnTimer = turnTimerUpdate;
+				gameHandler.sendAndApply(&ttu);
+			}
+			return true;
+		}
+		return false;
+	};
+	
+	if(state.human && si->turnTimerInfo.isBattleEnabled())
+	{
+		TurnTimerInfo turnTimer = state.turnTimer;
+		if(!turnTimerUpdateApplier(turnTimer))
+		{
+			if(turnTimer.battleTimer > 0)
+			{
+				turnTimer.creatureTimer = turnTimer.battleTimer;
+				turnTimer.battleTimer = 0;
+				turnTimerUpdateApplier(turnTimer);
+			}
+			else
+			{
+				BattleAction doNothing;
+				doNothing.actionType = EActionType::DEFEND;
+				doNothing.side = stack->unitSide();
+				doNothing.stackNumber = stack->unitId();
+				gameHandler.battles->makePlayerBattleAction(state.color, doNothing);
+			}
+		}
+	}
+}

+ 39 - 0
server/TurnTimerHandler.h

@@ -0,0 +1,39 @@
+/*
+ * TurnTimerHandler.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CStack;
+class PlayerColor;
+struct PlayerState;
+
+VCMI_LIB_NAMESPACE_END
+
+class CGameHandler;
+
+class TurnTimerHandler
+{
+	CGameHandler & gameHandler;
+	const int turnTimePropagateFrequency = 5000;
+	const int turnTimePropagateFrequencyCrit = 1000;
+	const int turnTimePropagateThreshold = 3000;
+	
+public:
+	TurnTimerHandler(CGameHandler &);
+	
+	void onGameplayStart(PlayerState & state);
+	void onPlayerGetTurn(PlayerState & state);
+	void onPlayerMakingTurn(PlayerState & state, int waitTime);
+	void onBattleStart();
+	void onBattleNextStack(const CStack & stack);
+	void onBattleLoop(int waitTime);
+};

+ 4 - 0
server/battles/BattleFlowProcessor.cpp

@@ -130,6 +130,8 @@ void BattleFlowProcessor::onBattleStarted()
 	assert(gameHandler->gameState()->curB);
 
 	tryPlaceMoats();
+	
+	gameHandler->turnTimerHandler.onBattleStart();
 
 	if (gameHandler->gameState()->curB->tacticDistance == 0)
 		onTacticsEnded();
@@ -315,6 +317,8 @@ void BattleFlowProcessor::activateNextStack()
 
 		if(!removeGhosts.changedStacks.empty())
 			gameHandler->sendAndApply(&removeGhosts);
+		
+		gameHandler->turnTimerHandler.onBattleNextStack(*next);
 
 		if (!tryMakeAutomaticAction(next))
 		{