1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-22 22:13:35 +02:00
vcmi/server/CVCMIServer.cpp
Ivan Savenko 14a3c6ad14 Fix freeze on closing server before starting the game
Fixes possible freeze that seems to be caused by client shutting down
socket before sending its final LobbyClientDisconnected packet, leading
to server not processing disconnection of host correctly, which in turn
causes client to wait server shutdown forever.

Looks like regression from #4722

- Fixes #4912 and its duplicates
2024-11-25 17:31:20 +00:00

1080 lines
28 KiB
C++

/*
* CVCMIServer.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 "CVCMIServer.h"
#include "CGameHandler.h"
#include "GlobalLobbyProcessor.h"
#include "LobbyNetPackVisitors.h"
#include "processors/PlayerMessageProcessor.h"
#include "../lib/CPlayerState.h"
#include "../lib/campaign/CampaignState.h"
#include "../lib/entities/hero/CHeroHandler.h"
#include "../lib/entities/hero/CHeroClass.h"
#include "../lib/gameState/CGameState.h"
#include "../lib/mapping/CMapDefines.h"
#include "../lib/mapping/CMapInfo.h"
#include "../lib/mapping/CMapHeader.h"
#include "../lib/rmg/CMapGenOptions.h"
#include "../lib/serializer/CMemorySerializer.h"
#include "../lib/serializer/Connection.h"
#include "../lib/texts/CGeneralTextHandler.h"
// UUID generation
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/program_options.hpp>
class CVCMIServerPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor)
{
private:
CVCMIServer & handler;
std::shared_ptr<CGameHandler> gh;
public:
CVCMIServerPackVisitor(CVCMIServer & handler, std::shared_ptr<CGameHandler> gh)
:handler(handler), gh(gh)
{
}
bool callTyped() override { return false; }
void visitForLobby(CPackForLobby & packForLobby) override
{
handler.handleReceivedPack(packForLobby);
}
void visitForServer(CPackForServer & serverPack) override
{
if (gh)
gh->handleReceivedPack(serverPack);
else
logNetwork->error("Received pack for game server while in lobby!");
}
void visitForClient(CPackForClient & clientPack) override
{
}
};
CVCMIServer::CVCMIServer(uint16_t port, bool runByClient)
: currentClientId(1)
, currentPlayerId(1)
, port(port)
, runByClient(runByClient)
{
uuid = boost::uuids::to_string(boost::uuids::random_generator()());
logNetwork->trace("CVCMIServer created! UUID: %s", uuid);
networkHandler = INetworkHandler::createHandler();
}
CVCMIServer::~CVCMIServer() = default;
uint16_t CVCMIServer::prepare(bool connectToLobby) {
if(connectToLobby) {
lobbyProcessor = std::make_unique<GlobalLobbyProcessor>(*this);
return 0;
} else {
return startAcceptingIncomingConnections();
}
}
uint16_t CVCMIServer::startAcceptingIncomingConnections()
{
port
? logNetwork->info("Port %d will be used", port)
: logNetwork->info("Randomly assigned port will be used");
// config port may be 0 => srvport will contain the OS-assigned port value
networkServer = networkHandler->createServerTCP(*this);
auto srvport = networkServer->start(port);
logNetwork->info("Listening for connections at port %d", srvport);
return srvport;
}
void CVCMIServer::onNewConnection(const std::shared_ptr<INetworkConnection> & connection)
{
if(getState() == EServerState::LOBBY)
{
activeConnections.push_back(std::make_shared<CConnection>(connection));
activeConnections.back()->enterLobbyConnectionMode();
}
else
{
// TODO: reconnection support
connection->close();
}
}
void CVCMIServer::onPacketReceived(const std::shared_ptr<INetworkConnection> & connection, const std::vector<std::byte> & message)
{
std::shared_ptr<CConnection> c = findConnection(connection);
if (c == nullptr)
throw std::out_of_range("Unknown connection received in CVCMIServer::findConnection");
auto pack = c->retrievePack(message);
pack->c = c;
CVCMIServerPackVisitor visitor(*this, this->gh);
pack->visit(visitor);
}
void CVCMIServer::setState(EServerState value)
{
if (value == EServerState::SHUTDOWN && state == EServerState::SHUTDOWN)
logGlobal->warn("Attempt to shutdown already shutdown server!");
// do not attempt to restart dying server
assert(state != EServerState::SHUTDOWN || state == value);
state = value;
if (state == EServerState::SHUTDOWN)
networkHandler->stop();
}
EServerState CVCMIServer::getState() const
{
return state;
}
std::shared_ptr<CConnection> CVCMIServer::findConnection(const std::shared_ptr<INetworkConnection> & netConnection)
{
for(const auto & gameConnection : activeConnections)
{
if (gameConnection->isMyConnection(netConnection))
return gameConnection;
}
return nullptr;
}
bool CVCMIServer::wasStartedByClient() const
{
return runByClient;
}
void CVCMIServer::run()
{
networkHandler->run();
}
void CVCMIServer::onTimer()
{
// we might receive onTimer call after transitioning from GAMEPLAY to LOBBY state, e.g. on game restart
if (getState() != EServerState::GAMEPLAY)
return;
static const auto serverUpdateInterval = std::chrono::milliseconds(100);
auto timeNow = std::chrono::steady_clock::now();
auto timePassedBefore = lastTimerUpdateTime - gameplayStartTime;
auto timePassedNow = timeNow - gameplayStartTime;
lastTimerUpdateTime = timeNow;
auto msPassedBefore = std::chrono::duration_cast<std::chrono::milliseconds>(timePassedBefore);
auto msPassedNow = std::chrono::duration_cast<std::chrono::milliseconds>(timePassedNow);
auto msDelta = msPassedNow - msPassedBefore;
if (msDelta.count())
gh->tick(msDelta.count());
networkHandler->createTimer(*this, serverUpdateInterval);
}
void CVCMIServer::prepareToRestart()
{
if(getState() != EServerState::GAMEPLAY)
{
assert(0);
return;
}
* si = * gh->gs->initialOpts;
setState(EServerState::LOBBY);
if (si->campState)
{
assert(si->campState->currentScenario().has_value());
campaignMap = si->campState->currentScenario().value_or(CampaignScenarioID(0));
campaignBonus = si->campState->getBonusID(campaignMap).value_or(-1);
}
for(auto activeConnection : activeConnections)
activeConnection->enterLobbyConnectionMode();
gh = nullptr;
}
bool CVCMIServer::prepareToStartGame()
{
Load::ProgressAccumulator progressTracking;
Load::Progress current(1);
progressTracking.include(current);
if (lobbyProcessor)
lobbyProcessor->sendGameStarted();
auto progressTrackingThread = boost::thread([this, &progressTracking]()
{
auto currentProgress = std::numeric_limits<Load::Type>::max();
while(!progressTracking.finished())
{
if(progressTracking.get() != currentProgress)
{
//FIXME: UNGUARDED MULTITHREADED ACCESS!!!
currentProgress = progressTracking.get();
LobbyLoadProgress loadProgress;
loadProgress.progress = currentProgress;
announcePack(loadProgress);
}
boost::this_thread::sleep(boost::posix_time::milliseconds(50));
}
});
gh = std::make_shared<CGameHandler>(this);
switch(si->mode)
{
case EStartMode::CAMPAIGN:
logNetwork->info("Preparing to start new campaign");
si->startTime = std::time(nullptr);
si->fileURI = mi->fileURI;
si->campState->setCurrentMap(campaignMap);
si->campState->setCurrentMapBonus(campaignBonus);
gh->init(si.get(), progressTracking);
break;
case EStartMode::NEW_GAME:
logNetwork->info("Preparing to start new game");
si->startTime = std::time(nullptr);
si->fileURI = mi->fileURI;
gh->init(si.get(), progressTracking);
break;
case EStartMode::LOAD_GAME:
logNetwork->info("Preparing to start loaded game");
if(!gh->load(si->mapname))
{
current.finish();
progressTrackingThread.join();
return false;
}
break;
default:
logNetwork->error("Wrong mode in StartInfo!");
assert(0);
break;
}
current.finish();
progressTrackingThread.join();
return true;
}
void CVCMIServer::startGameImmediately()
{
for(auto activeConnection : activeConnections)
activeConnection->enterGameplayConnectionMode(gh->gs);
gh->start(si->mode == EStartMode::LOAD_GAME);
setState(EServerState::GAMEPLAY);
lastTimerUpdateTime = gameplayStartTime = std::chrono::steady_clock::now();
onTimer();
multiplayerWelcomeMessage();
}
void CVCMIServer::onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage)
{
logNetwork->error("Network error receiving a pack. Connection has been closed");
std::shared_ptr<CConnection> c = findConnection(connection);
// player may have already disconnected via clientDisconnected call
if (c)
{
LobbyClientDisconnected lcd;
lcd.c = c;
lcd.clientId = c->connectionID;
handleReceivedPack(lcd);
}
}
void CVCMIServer::handleReceivedPack(CPackForLobby & pack)
{
ClientPermissionsCheckerNetPackVisitor checker(*this);
pack.visit(checker);
if(checker.getResult())
{
ApplyOnServerNetPackVisitor applier(*this);
pack.visit(applier);
if (applier.getResult())
announcePack(pack);
}
}
void CVCMIServer::announcePack(CPackForLobby & pack)
{
for(auto activeConnection : activeConnections)
{
// FIXME: we need to avoid sending something to client that not yet get answer for LobbyClientConnected
// Until UUID set we only pass LobbyClientConnected to this client
//if(c->uuid == uuid && !dynamic_cast<LobbyClientConnected *>(pack.get()))
// continue;
activeConnection->sendPack(pack);
}
ApplyOnServerAfterAnnounceNetPackVisitor applier(*this);
pack.visit(applier);
}
void CVCMIServer::announceMessage(const MetaString & txt)
{
logNetwork->info("Show message: %s", txt.toString());
LobbyShowMessage cm;
cm.message = txt;
announcePack(cm);
}
void CVCMIServer::announceMessage(const std::string & txt)
{
MetaString str;
str.appendRawString(txt);
announceMessage(str);
}
void CVCMIServer::announceTxt(const MetaString & txt, const std::string & playerName)
{
logNetwork->info("%s says: %s", playerName, txt.toString());
LobbyChatMessage cm;
cm.playerName = playerName;
cm.message = txt;
announcePack(cm);
}
void CVCMIServer::announceTxt(const std::string & txt, const std::string & playerName)
{
MetaString str;
str.appendRawString(txt);
announceTxt(str, playerName);
}
bool CVCMIServer::passHost(int toConnectionId)
{
for(auto activeConnection : activeConnections)
{
if(isClientHost(activeConnection->connectionID))
continue;
if(activeConnection->connectionID != toConnectionId)
continue;
hostClientId = activeConnection->connectionID;
announceTxt(boost::str(boost::format("Pass host to connection %d") % toConnectionId));
return true;
}
return false;
}
void CVCMIServer::clientConnected(std::shared_ptr<CConnection> c, std::vector<std::string> & names, const std::string & uuid, EStartMode mode)
{
assert(getState() == EServerState::LOBBY);
c->connectionID = currentClientId++;
if(hostClientId == -1)
{
hostClientId = c->connectionID;
si->mode = mode;
}
logNetwork->info("Connection with client %d established. UUID: %s", c->connectionID, c->uuid);
for(auto & name : names)
{
logNetwork->info("Client %d player: %s", c->connectionID, name);
ui8 id = currentPlayerId++;
ClientPlayer cp;
cp.connection = c->connectionID;
cp.name = name;
playerNames.insert(std::make_pair(id, cp));
announceTxt(boost::str(boost::format("%s (pid %d cid %d) joins the game") % name % id % c->connectionID));
//put new player in first slot with AI
for(auto & elem : si->playerInfos)
{
if(elem.second.isControlledByAI() && !elem.second.compOnly)
{
setPlayerConnectedId(elem.second, id);
break;
}
}
}
}
void CVCMIServer::clientDisconnected(std::shared_ptr<CConnection> connection)
{
assert(vstd::contains(activeConnections, connection));
logGlobal->trace("Received disconnection request");
vstd::erase(activeConnections, connection);
if(activeConnections.empty() || hostClientId == connection->connectionID)
{
setState(EServerState::SHUTDOWN);
return;
}
if(gh && getState() == EServerState::GAMEPLAY)
{
gh->handleClientDisconnection(connection);
}
// PlayerReinitInterface startAiPack;
// startAiPack.playerConnectionId = PlayerSettings::PLAYER_AI;
//
// for(auto it = playerNames.begin(); it != playerNames.end();)
// {
// if(it->second.connection != c->connectionID)
// {
// ++it;
// continue;
// }
//
// int id = it->first;
// std::string playerLeftMsgText = boost::str(boost::format("%s (pid %d cid %d) left the game") % id % playerNames[id].name % c->connectionID);
// announceTxt(playerLeftMsgText); //send lobby text, it will be ignored for non-lobby clients
// auto * playerSettings = si->getPlayersSettings(id);
// if(!playerSettings)
// {
// ++it;
// continue;
// }
//
// it = playerNames.erase(it);
// setPlayerConnectedId(*playerSettings, PlayerSettings::PLAYER_AI);
//
// if(gh && si && state == EServerState::GAMEPLAY)
// {
// gh->playerMessages->broadcastMessage(playerSettings->color, playerLeftMsgText);
// // gh->connections[playerSettings->color].insert(hostClient);
// startAiPack.players.push_back(playerSettings->color);
// }
// }
//
// if(!startAiPack.players.empty())
// gh->sendAndApply(startAiPack);
}
void CVCMIServer::reconnectPlayer(int connId)
{
PlayerReinitInterface startAiPack;
startAiPack.playerConnectionId = connId;
if(gh && si && getState() == EServerState::GAMEPLAY)
{
for(auto it = playerNames.begin(); it != playerNames.end(); ++it)
{
if(it->second.connection != connId)
continue;
int id = it->first;
auto * playerSettings = si->getPlayersSettings(id);
if(!playerSettings)
continue;
std::string messageText = boost::str(boost::format("%s (cid %d) is connected") % playerSettings->name % connId);
gh->playerMessages->broadcastMessage(playerSettings->color, messageText);
startAiPack.players.push_back(playerSettings->color);
}
if(!startAiPack.players.empty())
gh->sendAndApply(startAiPack);
}
}
void CVCMIServer::setPlayerConnectedId(PlayerSettings & pset, ui8 player) const
{
if(vstd::contains(playerNames, player))
pset.name = playerNames.find(player)->second.name;
else
pset.name = VLC->generaltexth->allTexts[468]; //Computer
pset.connectedPlayerIDs.clear();
if(player != PlayerSettings::PLAYER_AI)
pset.connectedPlayerIDs.insert(player);
}
void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr<CMapInfo> mapInfo, std::shared_ptr<CMapGenOptions> mapGenOpts)
{
mi = mapInfo;
if(!mi)
return;
auto namesIt = playerNames.cbegin();
si->playerInfos.clear();
if(mi->scenarioOptionsOfSave)
{
si = CMemorySerializer::deepCopy(*mi->scenarioOptionsOfSave);
si->mode = EStartMode::LOAD_GAME;
if(si->campState)
campaignMap = si->campState->currentScenario().value();
for(auto & ps : si->playerInfos)
{
if(!ps.second.compOnly && ps.second.connectedPlayerIDs.size() && namesIt != playerNames.cend())
{
setPlayerConnectedId(ps.second, namesIt++->first);
}
else
{
setPlayerConnectedId(ps.second, PlayerSettings::PLAYER_AI);
}
}
}
else if(si->mode == EStartMode::NEW_GAME || si->mode == EStartMode::CAMPAIGN)
{
if(mi->campaign)
return;
for(int i = 0; i < mi->mapHeader->players.size(); i++)
{
const PlayerInfo & pinfo = mi->mapHeader->players[i];
//neither computer nor human can play - no player
if(!(pinfo.canHumanPlay || pinfo.canComputerPlay))
continue;
PlayerSettings & pset = si->playerInfos[PlayerColor(i)];
pset.color = PlayerColor(i);
if(pinfo.canHumanPlay && namesIt != playerNames.cend())
{
setPlayerConnectedId(pset, namesIt++->first);
}
else
{
setPlayerConnectedId(pset, PlayerSettings::PLAYER_AI);
if(!pinfo.canHumanPlay)
{
pset.compOnly = true;
}
}
pset.castle = pinfo.defaultCastle();
pset.hero = pinfo.defaultHero();
if(pset.hero != HeroTypeID::RANDOM && pinfo.hasCustomMainHero())
{
pset.hero = pinfo.mainCustomHeroId;
pset.heroNameTextId = pinfo.mainCustomHeroNameTextId;
pset.heroPortrait = pinfo.mainCustomHeroPortrait;
}
}
if(mi->isRandomMap && mapGenOpts)
si->mapGenOptions = std::shared_ptr<CMapGenOptions>(mapGenOpts);
else
si->mapGenOptions.reset();
}
if (lobbyProcessor)
{
std::string roomDescription;
if (si->mapGenOptions)
{
if (si->mapGenOptions->getMapTemplate())
roomDescription = si->mapGenOptions->getMapTemplate()->getName();
// else - no template selected.
// TODO: handle this somehow?
}
else
roomDescription = mi->getNameTranslated();
lobbyProcessor->sendChangeRoomDescription(roomDescription);
}
si->mapname = mi->fileURI;
}
void CVCMIServer::updateAndPropagateLobbyState()
{
// Update player settings for RMG
// TODO: find appropriate location for this code
if(si->mapGenOptions && si->mode == EStartMode::NEW_GAME)
{
for(const auto & psetPair : si->playerInfos)
{
const auto & pset = psetPair.second;
si->mapGenOptions->setStartingTownForPlayer(pset.color, pset.castle);
si->mapGenOptions->setStartingHeroForPlayer(pset.color, pset.hero);
if(pset.isControlledByHuman())
{
si->mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::HUMAN);
}
else
{
si->mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::AI);
}
}
}
LobbyUpdateState lus;
lus.state = *this;
announcePack(lus);
}
void CVCMIServer::setPlayer(PlayerColor clickedColor)
{
struct PlayerToRestore
{
PlayerColor color;
int id;
void reset() { id = -1; color = PlayerColor::CANNOT_DETERMINE; }
PlayerToRestore(){ reset(); }
} playerToRestore;
PlayerSettings & clicked = si->playerInfos[clickedColor];
//identify clicked player
int clickedNameID = 0; //number of player - zero means AI, assume it initially
if(clicked.isControlledByHuman())
clickedNameID = *(clicked.connectedPlayerIDs.begin()); //if not AI - set appropriate ID
if(clickedNameID > 0 && playerToRestore.id == clickedNameID) //player to restore is about to being replaced -> put him back to the old place
{
PlayerSettings & restPos = si->playerInfos[playerToRestore.color];
setPlayerConnectedId(restPos, playerToRestore.id);
playerToRestore.reset();
}
int newPlayer; //which player will take clicked position
//who will be put here?
if(!clickedNameID) //AI player clicked -> if possible replace computer with unallocated player
{
newPlayer = getIdOfFirstUnallocatedPlayer();
if(!newPlayer) //no "free" player -> get just first one
newPlayer = playerNames.begin()->first;
}
else //human clicked -> take next
{
auto i = playerNames.find(clickedNameID); //clicked one
i++; //player AFTER clicked one
if(i != playerNames.end())
newPlayer = i->first;
else
newPlayer = 0; //AI if we scrolled through all players
}
setPlayerConnectedId(clicked, newPlayer); //put player
//if that player was somewhere else, we need to replace him with computer
if(newPlayer) //not AI
{
for(auto i = si->playerInfos.begin(); i != si->playerInfos.end(); i++)
{
int curNameID = *(i->second.connectedPlayerIDs.begin());
if(i->first != clickedColor && curNameID == newPlayer)
{
assert(i->second.connectedPlayerIDs.size());
playerToRestore.color = i->first;
playerToRestore.id = newPlayer;
setPlayerConnectedId(i->second, PlayerSettings::PLAYER_AI); //set computer
break;
}
}
}
}
void CVCMIServer::setPlayerName(PlayerColor color, std::string name)
{
if(color == PlayerColor::CANNOT_DETERMINE)
return;
PlayerSettings & player = si->playerInfos.at(color);
if(!player.isControlledByHuman())
return;
if(player.connectedPlayerIDs.empty())
return;
int nameID = *(player.connectedPlayerIDs.begin()); //if not AI - set appropriate ID
playerNames[nameID].name = name;
setPlayerConnectedId(player, nameID);
}
void CVCMIServer::setPlayerHandicap(PlayerColor color, Handicap handicap)
{
if(color == PlayerColor::CANNOT_DETERMINE)
return;
si->playerInfos[color].handicap = handicap;
int humanPlayer = 0;
for (const auto & pi : si->playerInfos)
if(pi.second.isControlledByHuman())
humanPlayer++;
if(humanPlayer < 2) // Singleplayer
return;
MetaString str;
str.appendTextID("vcmi.lobby.handicap");
str.appendRawString(" ");
str.appendName(color);
str.appendRawString(":");
if(handicap.startBonus.empty() && handicap.percentIncome == 100 && handicap.percentGrowth == 100)
{
str.appendRawString(" ");
str.appendTextID("core.genrltxt.523");
announceTxt(str);
return;
}
for(auto & res : EGameResID::ALL_RESOURCES())
if(handicap.startBonus[res] != 0)
{
str.appendRawString(" ");
str.appendName(res);
str.appendRawString(":");
str.appendRawString(std::to_string(handicap.startBonus[res]));
}
if(handicap.percentIncome != 100)
{
str.appendRawString(" ");
str.appendTextID("core.jktext.32");
str.appendRawString(":");
str.appendRawString(std::to_string(handicap.percentIncome) + "%");
}
if(handicap.percentGrowth != 100)
{
str.appendRawString(" ");
str.appendTextID("core.genrltxt.194");
str.appendRawString(":");
str.appendRawString(std::to_string(handicap.percentGrowth) + "%");
}
announceTxt(str);
}
void CVCMIServer::optionNextCastle(PlayerColor player, int dir)
{
PlayerSettings & s = si->playerInfos[player];
FactionID & cur = s.castle;
auto & allowed = getPlayerInfo(player).allowedFactions;
const bool allowRandomTown = getPlayerInfo(player).isFactionRandom;
if(cur == FactionID::NONE) //no change
return;
if(cur == FactionID::RANDOM) //first/last available
{
if(dir > 0)
cur = *allowed.begin(); //id of first town
else
cur = *allowed.rbegin(); //id of last town
}
else // next/previous available
{
if((cur == *allowed.begin() && dir < 0) || (cur == *allowed.rbegin() && dir > 0))
{
if(allowRandomTown)
{
cur = FactionID::RANDOM;
}
else
{
if(dir > 0)
cur = *allowed.begin();
else
cur = *allowed.rbegin();
}
}
else
{
assert(dir >= -1 && dir <= 1); //othervice std::advance may go out of range
auto iter = allowed.find(cur);
std::advance(iter, dir);
cur = *iter;
}
}
if(s.hero.isValid() && !getPlayerInfo(player).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor
{
s.hero = HeroTypeID::RANDOM;
}
if(!cur.isValid() && s.bonus == PlayerStartingBonus::RESOURCE)
s.bonus = PlayerStartingBonus::RANDOM;
}
void CVCMIServer::optionSetCastle(PlayerColor player, FactionID id)
{
PlayerSettings & s = si->playerInfos[player];
FactionID & cur = s.castle;
auto & allowed = getPlayerInfo(player).allowedFactions;
if(cur == FactionID::NONE) //no change
return;
if(allowed.find(id) == allowed.end() && id != FactionID::RANDOM) // valid id
return;
cur = static_cast<FactionID>(id);
if(s.hero.isValid() && !getPlayerInfo(player).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor
{
s.hero = HeroTypeID::RANDOM;
}
if(!cur.isValid() && s.bonus == PlayerStartingBonus::RESOURCE)
s.bonus = PlayerStartingBonus::RANDOM;
}
void CVCMIServer::setCampaignMap(CampaignScenarioID mapId)
{
campaignMap = mapId;
si->difficulty = si->campState->scenario(mapId).difficulty;
campaignBonus = -1;
updateStartInfoOnMapChange(si->campState->getMapInfo(mapId));
}
void CVCMIServer::setCampaignBonus(int bonusId)
{
campaignBonus = bonusId;
const CampaignScenario & scenario = si->campState->scenario(campaignMap);
const std::vector<CampaignBonus> & bonDescs = scenario.travelOptions.bonusesToChoose;
if(bonDescs[bonusId].type == CampaignBonusType::HERO)
{
for(auto & elem : si->playerInfos)
{
if(elem.first == PlayerColor(bonDescs[bonusId].info1))
setPlayerConnectedId(elem.second, 1);
else
setPlayerConnectedId(elem.second, PlayerSettings::PLAYER_AI);
}
}
}
void CVCMIServer::optionNextHero(PlayerColor player, int dir)
{
PlayerSettings & s = si->playerInfos[player];
if(!s.castle.isValid() || s.hero == HeroTypeID::NONE)
return;
if(s.hero == HeroTypeID::RANDOM) // first/last available
{
if (dir > 0)
s.hero = nextAllowedHero(player, HeroTypeID(-1), dir);
else
s.hero = nextAllowedHero(player, HeroTypeID(VLC->heroh->size()), dir);
}
else
{
s.hero = nextAllowedHero(player, s.hero, dir);
}
}
void CVCMIServer::optionSetHero(PlayerColor player, HeroTypeID id)
{
PlayerSettings & s = si->playerInfos[player];
if(!s.castle.isValid() || s.hero == HeroTypeID::NONE)
return;
if(id == HeroTypeID::RANDOM)
{
s.hero = HeroTypeID::RANDOM;
}
if(canUseThisHero(player, id))
s.hero = static_cast<HeroTypeID>(id);
}
HeroTypeID CVCMIServer::nextAllowedHero(PlayerColor player, HeroTypeID initial, int direction)
{
HeroTypeID first(initial.getNum() + direction);
if(direction > 0)
{
for (auto i = first; i.getNum() < VLC->heroh->size(); ++i)
if(canUseThisHero(player, i))
return i;
}
else
{
for (auto i = first; i.getNum() >= 0; --i)
if(canUseThisHero(player, i))
return i;
}
return HeroTypeID::RANDOM;
}
void CVCMIServer::optionNextBonus(PlayerColor player, int dir)
{
PlayerSettings & s = si->playerInfos[player];
PlayerStartingBonus & ret = s.bonus = static_cast<PlayerStartingBonus>(static_cast<int>(s.bonus) + dir);
if(s.hero == HeroTypeID::NONE &&
!getPlayerInfo(player).heroesNames.size() &&
ret == PlayerStartingBonus::ARTIFACT) //no hero - can't be artifact
{
if(dir < 0)
ret = PlayerStartingBonus::RANDOM;
else
ret = PlayerStartingBonus::GOLD;
}
if(ret > PlayerStartingBonus::RESOURCE)
ret = PlayerStartingBonus::RANDOM;
if(ret < PlayerStartingBonus::RANDOM)
ret = PlayerStartingBonus::RESOURCE;
if(s.castle == FactionID::RANDOM && ret == PlayerStartingBonus::RESOURCE) //random castle - can't be resource
{
if(dir < 0)
ret = PlayerStartingBonus::GOLD;
else
ret = PlayerStartingBonus::RANDOM;
}
}
void CVCMIServer::optionSetBonus(PlayerColor player, PlayerStartingBonus id)
{
PlayerSettings & s = si->playerInfos[player];
if(s.hero == HeroTypeID::NONE &&
!getPlayerInfo(player).heroesNames.size() &&
id == PlayerStartingBonus::ARTIFACT) //no hero - can't be artifact
return;
if(id > PlayerStartingBonus::RESOURCE)
return;
if(id < PlayerStartingBonus::RANDOM)
return;
if(s.castle == FactionID::RANDOM && id == PlayerStartingBonus::RESOURCE) //random castle - can't be resource
return;
s.bonus = id;
}
bool CVCMIServer::canUseThisHero(PlayerColor player, HeroTypeID ID)
{
return VLC->heroh->size() > ID
&& si->playerInfos[player].castle == VLC->heroh->objects[ID]->heroClass->faction
&& !vstd::contains(getUsedHeroes(), ID)
&& mi->mapHeader->allowedHeroes.count(ID);
}
std::vector<HeroTypeID> CVCMIServer::getUsedHeroes()
{
std::vector<HeroTypeID> heroIds;
for(auto & p : si->playerInfos)
{
const auto & heroes = getPlayerInfo(p.first).heroesNames;
for(auto & hero : heroes)
if(hero.heroId >= 0) //in VCMI map format heroId = -1 means random hero
heroIds.push_back(hero.heroId);
if(p.second.hero != HeroTypeID::RANDOM)
heroIds.push_back(p.second.hero);
}
return heroIds;
}
ui8 CVCMIServer::getIdOfFirstUnallocatedPlayer() const
{
for(auto i = playerNames.cbegin(); i != playerNames.cend(); i++)
{
if(!si->getPlayersSettings(i->first))
return i->first;
}
return 0;
}
void CVCMIServer::multiplayerWelcomeMessage()
{
int humanPlayer = 0;
for (const auto & pi : si->playerInfos)
if(pi.second.isControlledByHuman())
humanPlayer++;
if(humanPlayer < 2) // Singleplayer
return;
gh->playerMessages->broadcastSystemMessage("Use '!help' to list available commands");
for (const auto & pi : si->playerInfos)
if(!pi.second.handicap.startBonus.empty() || pi.second.handicap.percentIncome != 100 || pi.second.handicap.percentGrowth != 100)
{
MetaString str;
str.appendTextID("vcmi.lobby.handicap");
str.appendRawString(" ");
str.appendName(pi.first);
str.appendRawString(":");
for(auto & res : EGameResID::ALL_RESOURCES())
if(pi.second.handicap.startBonus[res] != 0)
{
str.appendRawString(" ");
str.appendName(res);
str.appendRawString(":");
str.appendRawString(std::to_string(pi.second.handicap.startBonus[res]));
}
if(pi.second.handicap.percentIncome != 100)
{
str.appendRawString(" ");
str.appendTextID("core.jktext.32");
str.appendRawString(":");
str.appendRawString(std::to_string(pi.second.handicap.percentIncome) + "%");
}
if(pi.second.handicap.percentGrowth != 100)
{
str.appendRawString(" ");
str.appendTextID("core.genrltxt.194");
str.appendRawString(":");
str.appendRawString(std::to_string(pi.second.handicap.percentGrowth) + "%");
}
gh->playerMessages->broadcastSystemMessage(str);
}
std::vector<std::string> optionIds;
if(si->extraOptionsInfo.cheatsAllowed)
optionIds.emplace_back("vcmi.optionsTab.cheatAllowed.hover");
if(si->extraOptionsInfo.unlimitedReplay)
optionIds.emplace_back("vcmi.optionsTab.unlimitedReplay.hover");
if(!optionIds.size()) // No settings to publish
return;
MetaString str;
str.appendTextID("vcmi.optionsTab.extraOptions.hover");
str.appendRawString(": ");
for(int i = 0; i < optionIds.size(); i++)
{
str.appendTextID(optionIds[i]);
if(i < optionIds.size() - 1)
str.appendRawString(", ");
}
gh->playerMessages->broadcastSystemMessage(str);
}
INetworkHandler & CVCMIServer::getNetworkHandler()
{
return *networkHandler;
}