1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-20 20:23:03 +02:00

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

This commit is contained in:
Ivan Savenko 2024-05-19 17:12:29 +00:00
parent d5f32c27b0
commit 10b50cd905
8 changed files with 306 additions and 49 deletions

View File

@ -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
};

View File

@ -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");

View File

@ -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)

View File

@ -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)
{

View File

@ -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;
broadcastSystemMessage("game was terminated");
gameHandler->gameLobby()->setState(EServerState::SHUTDOWN);
}
if(words[1] == "exit" || words[1] == "quit" || words[1] == "end")
void PlayerMessageProcessor::commandKick(PlayerColor player, const std::vector<std::string> & words)
{
bool isHost = gameHandler->gameLobby()->isPlayerHost(player);
if(!isHost)
return;
if(words.size() == 2)
{
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]);
return true;
}
if(words.size() == 3 && words[1] == "kick")
{
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)
{
int playersCheated = 0;
for (const auto & player : gameHandler->gameState()->players)
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)
{
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!");
}
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");
}
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;
}
if(words[1] == "yes" || words[1] == "no")
{
if(currentVote == ECurrentChatVote::NONE)
{
broadcastSystemMessage("No active voting!");
return;
}
if (!playersCheated)
broadcastSystemMessage("No cheaters registered!");
return true;
if(words[1] == "yes")
{
awaitingPlayers.erase(player);
if(awaitingPlayers.empty())
finishVoting();
return;
}
if(words[1] == "no")
{
abortVoting();
return;
}
}
return false;
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)

View File

@ -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);

View File

@ -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;
}

View File

@ -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;
}
}
};