ソースを参照

It is now possible to vote for simturns and turn timer changes

Ivan Savenko 1 年間 前
コミット
10b50cd905

+ 1 - 0
lib/serializer/ESerializationVersion.h

@@ -43,6 +43,7 @@ enum class ESerializationVersion : int32_t
 	ARTIFACT_COSTUMES, // 840 swappable artifacts set added
 
 	RELEASE_150 = ARTIFACT_COSTUMES, // for convenience
+	VOTING_SIMTURNS, // 841 - allow modification of simturns duration via vote
 
 	CURRENT = ARTIFACT_COSTUMES
 };

+ 2 - 0
server/CVCMIServer.cpp

@@ -992,6 +992,8 @@ void CVCMIServer::multiplayerWelcomeMessage()
 	if(humanPlayer < 2) // Singleplayer
 		return;
 
+	gh->playerMessages->broadcastSystemMessage("Use '!help' to list available commands");
+
 	std::vector<std::string> optionIds;
 	if(si->extraOptionsInfo.cheatsAllowed)
 		optionIds.push_back("vcmi.optionsTab.cheatAllowed.hover");

+ 9 - 3
server/TurnTimerHandler.cpp

@@ -81,14 +81,20 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player)
 	}
 }
 
-void TurnTimerHandler::update(int waitTime)
+void TurnTimerHandler::prolongTimers(int durationMs)
+{
+	for (auto & timer : timers)
+		timer.second.baseTimer += durationMs;
+}
+
+void TurnTimerHandler::update(int waitTimeMs)
 {
 	if(!gameHandler.getStartInfo()->turnTimerInfo.isEnabled())
 		return;
 
 	for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
 		if(gameHandler.gameState()->isPlayerMakingTurn(player))
-			onPlayerMakingTurn(player, waitTime);
+			onPlayerMakingTurn(player, waitTimeMs);
 
 	// create copy for iterations - battle might end during onBattleLoop call
 	std::vector<BattleID> ongoingBattles;
@@ -97,7 +103,7 @@ void TurnTimerHandler::update(int waitTime)
 		ongoingBattles.push_back(battle->battleID);
 
 	for (auto & battleID : ongoingBattles)
-		onBattleLoop(battleID, waitTime);
+		onBattleLoop(battleID, waitTimeMs);
 }
 
 bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor player, int waitTime)

+ 3 - 1
server/TurnTimerHandler.h

@@ -45,10 +45,12 @@ public:
 	void onBattleStart(const BattleID & battle);
 	void onBattleNextStack(const BattleID & battle, const CStack & stack);
 	void onBattleEnd(const BattleID & battleID);
-	void update(int waitTime);
+	void update(int waitTimeMs);
 	void setTimerEnabled(PlayerColor player, bool enabled);
 	void setEndTurnAllowed(PlayerColor player, bool enabled);
 
+	void prolongTimers(int durationMs);
+
 	template<typename Handler>
 	void serialize(Handler & h)
 	{

+ 235 - 42
server/processors/PlayerMessageProcessor.cpp

@@ -10,14 +10,14 @@
 #include "StdInc.h"
 #include "PlayerMessageProcessor.h"
 
+#include "TurnOrderProcessor.h"
+
 #include "../CGameHandler.h"
 #include "../CVCMIServer.h"
+#include "../TurnTimerHandler.h"
 
-#include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CHeroHandler.h"
-#include "../../lib/modding/IdentifierStorage.h"
 #include "../../lib/CPlayerState.h"
-#include "../../lib/GameConstants.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
@@ -34,22 +34,26 @@ PlayerMessageProcessor::PlayerMessageProcessor(CGameHandler * gameHandler)
 {
 }
 
-void PlayerMessageProcessor::playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj)
+void PlayerMessageProcessor::playerMessage(PlayerColor player, const std::string & message, ObjectInstanceID currObj)
 {
-	if (handleHostCommand(player, message))
+	if(!message.empty() && message[0] == '!')
+	{
+		broadcastMessage(player, message);
+		handleCommand(player, message);
 		return;
+	}
 
-	if (handleCheatCode(message, player, currObj))
+	if(handleCheatCode(message, player, currObj))
 	{
 		if(!gameHandler->getPlayerSettings(player)->isControlledByAI())
 		{
 			MetaString txt;
 			txt.appendLocalString(EMetaText::GENERAL_TXT, 260);
 			broadcastSystemMessage(txt);
-		}			
+		}
 
 		if(!player.isSpectator())
-			gameHandler->checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature
+			gameHandler->checkVictoryLossConditionsForPlayer(player); //Player enter win code or got required art\creature
 
 		return;
 	}
@@ -57,33 +61,25 @@ void PlayerMessageProcessor::playerMessage(PlayerColor player, const std::string
 	broadcastMessage(player, message);
 }
 
-bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::string &message)
+void PlayerMessageProcessor::commandExit(PlayerColor player, const std::vector<std::string> & words)
 {
-	std::vector<std::string> words;
-	boost::split(words, message, boost::is_any_of(" "));
-
 	bool isHost = gameHandler->gameLobby()->isPlayerHost(player);
+	if(!isHost)
+		return;
 
-	if(!isHost || words.size() < 2 || words[0] != "game")
-		return false;
-
-	if(words[1] == "exit" || words[1] == "quit" || words[1] == "end")
-	{
-		broadcastSystemMessage("game was terminated");
-		gameHandler->gameLobby()->setState(EServerState::SHUTDOWN);
+	broadcastSystemMessage("game was terminated");
+	gameHandler->gameLobby()->setState(EServerState::SHUTDOWN);
+}
 
-		return true;
-	}
-	if(words.size() == 3 && words[1] == "save")
-	{
-		gameHandler->save("Saves/" + words[2]);
-		broadcastSystemMessage("game saved as " + words[2]);
+void PlayerMessageProcessor::commandKick(PlayerColor player, const std::vector<std::string> & words)
+{
+	bool isHost = gameHandler->gameLobby()->isPlayerHost(player);
+	if(!isHost)
+		return;
 
-		return true;
-	}
-	if(words.size() == 3 && words[1] == "kick")
+	if(words.size() == 2)
 	{
-		auto playername = words[2];
+		auto playername = words[1];
 		PlayerColor playerToKick(PlayerColor::CANNOT_DETERMINE);
 		if(std::all_of(playername.begin(), playername.end(), ::isdigit))
 			playerToKick = PlayerColor(std::stoi(playername));
@@ -104,27 +100,224 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st
 			gameHandler->sendAndApply(&pc);
 			gameHandler->checkVictoryLossConditionsForPlayer(playerToKick);
 		}
-		return true;
 	}
-	if(words.size() == 2 && words[1] == "cheaters")
+}
+
+void PlayerMessageProcessor::commandSave(PlayerColor player, const std::vector<std::string> & words)
+{
+	bool isHost = gameHandler->gameLobby()->isPlayerHost(player);
+	if(!isHost)
+		return;
+
+	if(words.size() == 2)
+	{
+		gameHandler->save("Saves/" + words[1]);
+		broadcastSystemMessage("game saved as " + words[1]);
+	}
+}
+
+void PlayerMessageProcessor::commandCheaters(PlayerColor player, const std::vector<std::string> & words)
+{
+	int playersCheated = 0;
+	for(const auto & player : gameHandler->gameState()->players)
 	{
-		int playersCheated = 0;
-		for (const auto & player : gameHandler->gameState()->players)
+		if(player.second.cheated)
 		{
-			if(player.second.cheated)
-			{
-				broadcastSystemMessage("Player " + player.first.toString() + " is cheater!");
-				playersCheated++;
-			}
+			broadcastSystemMessage("Player " + player.first.toString() + " is cheater!");
+			playersCheated++;
 		}
+	}
+
+	if(!playersCheated)
+		broadcastSystemMessage("No cheaters registered!");
+}
 
-		if (!playersCheated)
-			broadcastSystemMessage("No cheaters registered!");
+void PlayerMessageProcessor::commandHelp(PlayerColor player, const std::vector<std::string> & words)
+{
+	broadcastSystemMessage("Available commands to host:");
+	broadcastSystemMessage("'!exit' - immediately ends current game");
+	broadcastSystemMessage("'!kick <player>' - kick specified player from the game");
+	broadcastSystemMessage("'!save <filename>' - save game under specified filename");
+	broadcastSystemMessage("Available commands to all players:");
+	broadcastSystemMessage("'!help' - display this help");
+	broadcastSystemMessage("'!cheaters' - list players that entered cheat command during game");
+	broadcastSystemMessage("'!vote' - allows to change some game settings if all players vote for it");
+}
 
-		return true;
+void PlayerMessageProcessor::commandVote(PlayerColor player, const std::vector<std::string> & words)
+{
+	if(words.size() < 2)
+	{
+		broadcastSystemMessage("'!vote simturns allow X' - allow simultaneous turns for specified number of days, or until contact");
+		broadcastSystemMessage("'!vote simturns force X' - force simultaneous turns for specified number of days, blocking player contacts");
+		broadcastSystemMessage("'!vote simturns abort' - abort simultaneous turns once this turn ends");
+		broadcastSystemMessage("'!vote timer prolong X' - prolong base timer for all players by specified number of seconds");
+		return;
 	}
 
-	return false;
+	if(words[1] == "yes" || words[1] == "no")
+	{
+		if(currentVote == ECurrentChatVote::NONE)
+		{
+			broadcastSystemMessage("No active voting!");
+			return;
+		}
+
+		if(words[1] == "yes")
+		{
+			awaitingPlayers.erase(player);
+			if(awaitingPlayers.empty())
+				finishVoting();
+			return;
+		}
+		if(words[1] == "no")
+		{
+			abortVoting();
+			return;
+		}
+	}
+
+	const auto & parseNumber = [](const std::string & input) -> std::optional<int>
+	{
+		try
+		{
+			return std::stol(input);
+		}
+		catch(std::logic_error &)
+		{
+			return std::nullopt;
+		}
+	};
+
+	if(words[1] == "simturns" && words.size() > 2)
+	{
+		if(words[2] == "allow" && words.size() > 3)
+		{
+			auto daysCount = parseNumber(words[3]);
+			if(daysCount && daysCount.value() > 0)
+				startVoting(player, ECurrentChatVote::SIMTURNS_ALLOW, daysCount.value());
+			return;
+		}
+
+		if(words[2] == "force" && words.size() > 3)
+		{
+			auto daysCount = parseNumber(words[3]);
+			if(daysCount && daysCount.value() > 0)
+				startVoting(player, ECurrentChatVote::SIMTURNS_FORCE, daysCount.value());
+			return;
+		}
+
+		if(words[2] == "abort")
+		{
+			startVoting(player, ECurrentChatVote::SIMTURNS_ABORT, 0);
+			return;
+		}
+	}
+
+	if(words[1] == "timer" && words.size() > 2)
+	{
+		if(words[2] == "prolong" && words.size() > 3)
+		{
+			auto secondsCount = parseNumber(words[3]);
+			if(secondsCount && secondsCount.value() > 0)
+				startVoting(player, ECurrentChatVote::TIMER_PROLONG, secondsCount.value());
+			return;
+		}
+	}
+
+	broadcastSystemMessage("Voting command not recognized!");
+}
+
+void PlayerMessageProcessor::finishVoting()
+{
+	switch(currentVote)
+	{
+		case ECurrentChatVote::SIMTURNS_ALLOW:
+			broadcastSystemMessage("Voting successful. Simultaneous turns will run for " + std::to_string(currentVoteParameter) + " more days, or until contact");
+			gameHandler->turnOrder->setMaxSimturnsDuration(currentVoteParameter);
+			break;
+		case ECurrentChatVote::SIMTURNS_FORCE:
+			broadcastSystemMessage("Voting successful. Simultaneous turns will run for " + std::to_string(currentVoteParameter) + " more days. Contacts are blocked");
+			gameHandler->turnOrder->setMinSimturnsDuration(currentVoteParameter);
+			break;
+		case ECurrentChatVote::SIMTURNS_ABORT:
+			broadcastSystemMessage("Voting successful. Simultaneous turns will end on next day");
+			gameHandler->turnOrder->setMinSimturnsDuration(0);
+			gameHandler->turnOrder->setMaxSimturnsDuration(0);
+			break;
+		case ECurrentChatVote::TIMER_PROLONG:
+			broadcastSystemMessage("Voting successful. Timer for all players has been prolonger for " + std::to_string(currentVoteParameter) + " seconds");
+			gameHandler->turnTimerHandler->prolongTimers(currentVoteParameter * 1000);
+			break;
+	}
+
+	currentVote = ECurrentChatVote::NONE;
+	currentVoteParameter = -1;
+}
+
+void PlayerMessageProcessor::abortVoting()
+{
+	broadcastSystemMessage("Player voted against change. Voting aborted");
+	currentVote = ECurrentChatVote::NONE;
+}
+
+void PlayerMessageProcessor::startVoting(PlayerColor initiator, ECurrentChatVote what, int parameter)
+{
+	currentVote = what;
+	currentVoteParameter = parameter;
+
+	switch(currentVote)
+	{
+		case ECurrentChatVote::SIMTURNS_ALLOW:
+			broadcastSystemMessage("Started voting to allow simultaneous turns for " + std::to_string(parameter) + " more days");
+			break;
+		case ECurrentChatVote::SIMTURNS_FORCE:
+			broadcastSystemMessage("Started voting to force simultaneous turns for " + std::to_string(parameter) + " more days");
+			break;
+		case ECurrentChatVote::SIMTURNS_ABORT:
+			broadcastSystemMessage("Started voting to end simultaneous turns starting from next day");
+			break;
+		case ECurrentChatVote::TIMER_PROLONG:
+			broadcastSystemMessage("Started voting to prolong timer for all players by " + std::to_string(parameter) + " seconds");
+			break;
+		default:
+			return;
+	}
+
+	broadcastSystemMessage("Type '!vote yes' to agree to this change or '!vote no' to vote against it");
+	awaitingPlayers.clear();
+
+	for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
+	{
+		auto state = gameHandler->getPlayerState(player, false);
+		if(state && state->isHuman() && initiator != player)
+			awaitingPlayers.insert(player);
+	}
+
+	if(awaitingPlayers.empty())
+		finishVoting();
+}
+
+void PlayerMessageProcessor::handleCommand(PlayerColor player, const std::string & message)
+{
+	if(message.empty() || message[0] != '!')
+		return;
+
+	std::vector<std::string> words;
+	boost::split(words, message, boost::is_any_of(" "));
+
+	if(words[0] == "!exit" || words[0] == "!quit")
+		commandExit(player, words);
+	if(words[0] == "!help")
+		commandHelp(player, words);
+	if(words[0] == "!vote")
+		commandVote(player, words);
+	if(words[0] == "!kick")
+		commandKick(player, words);
+	if(words[0] == "!save")
+		commandSave(player, words);
+	if(words[0] == "!cheaters")
+		commandCheaters(player, words);
 }
 
 void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero)

+ 25 - 1
server/processors/PlayerMessageProcessor.h

@@ -20,13 +20,26 @@ VCMI_LIB_NAMESPACE_END
 
 class CGameHandler;
 
+enum class ECurrentChatVote : int8_t
+{
+	NONE = -1,
+	SIMTURNS_ALLOW,
+	SIMTURNS_FORCE,
+	SIMTURNS_ABORT,
+	TIMER_PROLONG,
+};
+
 class PlayerMessageProcessor
 {
 	CGameHandler * gameHandler;
 
+	ECurrentChatVote currentVote = ECurrentChatVote::NONE;
+	int currentVoteParameter = 0;
+	std::set<PlayerColor> awaitingPlayers;
+
 	void executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector<std::string> & arguments );
 	bool handleCheatCode(const std::string & cheatFullCommand, PlayerColor player, ObjectInstanceID currObj);
-	bool handleHostCommand(PlayerColor player, const std::string & message);
+	void handleCommand(PlayerColor player, const std::string & message);
 
 	void cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero);
 	void cheatBuildTown(PlayerColor player, const CGTownInstance * town);
@@ -45,6 +58,17 @@ class PlayerMessageProcessor
 	void cheatMaxMorale(PlayerColor player, const CGHeroInstance * hero);
 	void cheatFly(PlayerColor player, const CGHeroInstance * hero);
 
+	void commandExit(PlayerColor player, const std::vector<std::string> & words);
+	void commandKick(PlayerColor player, const std::vector<std::string> & words);
+	void commandSave(PlayerColor player, const std::vector<std::string> & words);
+	void commandCheaters(PlayerColor player, const std::vector<std::string> & words);
+	void commandHelp(PlayerColor player, const std::vector<std::string> & words);
+	void commandVote(PlayerColor player, const std::vector<std::string> & words);
+
+	void finishVoting();
+	void abortVoting();
+	void startVoting(PlayerColor initiator, ECurrentChatVote what, int parameter);
+
 public:
 	PlayerMessageProcessor(CGameHandler * gameHandler);
 

+ 14 - 0
server/processors/TurnOrderProcessor.cpp

@@ -28,11 +28,15 @@ TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner):
 
 int TurnOrderProcessor::simturnsTurnsMaxLimit() const
 {
+	if (simturnsMaxDurationDays)
+		return *simturnsMaxDurationDays;
 	return gameHandler->getStartInfo()->simturnsInfo.optionalTurns;
 }
 
 int TurnOrderProcessor::simturnsTurnsMinLimit() const
 {
+	if (simturnsMinDurationDays)
+		return *simturnsMinDurationDays;
 	return gameHandler->getStartInfo()->simturnsInfo.requiredTurns;
 }
 
@@ -373,3 +377,13 @@ bool TurnOrderProcessor::isPlayerAwaitsNewDay(PlayerColor which) const
 {
 	return vstd::contains(actedPlayers, which);
 }
+
+void TurnOrderProcessor::setMinSimturnsDuration(int days)
+{
+	simturnsMinDurationDays = gameHandler->getDate(Date::DAY) + days;
+}
+
+void TurnOrderProcessor::setMaxSimturnsDuration(int days)
+{
+	simturnsMaxDurationDays = gameHandler->getDate(Date::DAY) + days;
+}

+ 15 - 0
server/processors/TurnOrderProcessor.h

@@ -41,6 +41,9 @@ class TurnOrderProcessor : boost::noncopyable
 	std::set<PlayerColor> actingPlayers;
 	std::set<PlayerColor> actedPlayers;
 
+	std::optional<int> simturnsMinDurationDays;
+	std::optional<int> simturnsMaxDurationDays;
+
 	/// Returns date on which simturns must end unconditionally
 	int simturnsTurnsMaxLimit() const;
 
@@ -91,6 +94,12 @@ public:
 	/// Start game (or resume from save) and send PlayerStartsTurn pack to player(s)
 	void onGameStarted();
 
+	/// Permanently override duration of contactless simultaneous turns
+	void setMinSimturnsDuration(int days);
+
+	/// Permanently override duration of simultaneous turns with contact detection
+	void setMaxSimturnsDuration(int days);
+
 	template<typename Handler>
 	void serialize(Handler & h)
 	{
@@ -98,5 +107,11 @@ public:
 		h & awaitingPlayers;
 		h & actingPlayers;
 		h & actedPlayers;
+
+		if (h.version >= Handler::Version::VOTING_SIMTURNS)
+		{
+			h & simturnsMinDurationDays;
+			h & simturnsMaxDurationDays;
+		}
 	}
 };