#include "StdInc.h" #include "CMusicHandler.h" #include "../lib/mapping/CCampaignHandler.h" #include "../CCallback.h" #include "../lib/CConsoleHandler.h" #include "CGameInfo.h" #include "../lib/CGameState.h" #include "CPlayerInterface.h" #include "../lib/StartInfo.h" #include "../lib/BattleState.h" #include "../lib/CModHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CDefObjInfoHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/CTownHandler.h" #include "../lib/CObjectHandler.h" #include "../lib/CBuildingHandler.h" #include "../lib/CSpellHandler.h" #include "../lib/Connection.h" #include "../lib/Interprocess.h" #include "../lib/NetPacks.h" #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" #include "../lib/mapping/CMap.h" #include "../lib/JsonNode.h" #include "mapHandler.h" #include "../lib/CConfigHandler.h" #include "Client.h" #include "CPreGame.h" #include "battle/CBattleInterface.h" #include "../lib/CThreadHelper.h" #include "../lib/CScriptingModule.h" #include "../lib/RegisterTypes.h" #include "gui/CGuiHandler.h" #include "CMT.h" extern std::string NAME; namespace intpr = boost::interprocess; /* * Client.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 * */ template class CApplyOnCL; class CBaseForCLApply { public: virtual void applyOnClAfter(CClient *cl, void *pack) const =0; virtual void applyOnClBefore(CClient *cl, void *pack) const =0; virtual ~CBaseForCLApply(){} template static CBaseForCLApply *getApplier(const U * t=nullptr) { return new CApplyOnCL; } }; template class CApplyOnCL : public CBaseForCLApply { public: void applyOnClAfter(CClient *cl, void *pack) const { T *ptr = static_cast(pack); ptr->applyCl(cl); } void applyOnClBefore(CClient *cl, void *pack) const { T *ptr = static_cast(pack); ptr->applyFirstCl(cl); } }; static CApplier *applier = nullptr; void CClient::init() { hotSeat = false; connectionHandler = nullptr; pathInfo = nullptr; applier = new CApplier; registerTypes2(*applier); IObjectInterface::cb = this; serv = nullptr; gs = nullptr; erm = nullptr; terminate = false; } CClient::CClient(void) { init(); } CClient::CClient(CConnection *con, StartInfo *si) { init(); newGame(con,si); } CClient::~CClient(void) { delete applier; } void CClient::waitForMoveAndSend(PlayerColor color) { try { setThreadName("CClient::waitForMoveAndSend"); assert(vstd::contains(battleints, color)); BattleAction ba = battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false)); logNetwork->traceStream() << "Send battle action to server: " << ba; MakeAction temp_action(ba); sendRequest(&temp_action, color); return; } catch(boost::thread_interrupted&) { logNetwork->debugStream() << "Wait for move thread was interrupted and no action will be send. Was a battle ended by spell?"; return; } HANDLE_EXCEPTION logNetwork->errorStream() << "We should not be here!"; } void CClient::run() { setThreadName("CClient::run"); try { while(!terminate) { CPack *pack = serv->retreivePack(); //get the package from the server if (terminate) { vstd::clear_pointer(pack); break; } handlePack(pack); } } //catch only asio exceptions catch (const boost::system::system_error& e) { logNetwork->errorStream() << "Lost connection to server, ending listening thread!"; logNetwork->errorStream() << e.what(); if(!terminate) //rethrow (-> boom!) only if closing connection was unexpected { logNetwork->errorStream() << "Something wrong, lost connection while game is still ongoing..."; throw; } } } void CClient::save(const std::string & fname) { if(gs->curB) { logNetwork->errorStream() << "Game cannot be saved during battle!"; return; } SaveGame save_game(fname); sendRequest((CPackForClient*)&save_game, PlayerColor::NEUTRAL); } void CClient::endGame( bool closeConnection /*= true*/ ) { //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) for(auto i : playerint) i.second->finish(); // Game is ending // Tell the network thread to reach a stable state if(closeConnection) stopConnection(); logNetwork->infoStream() << "Closed connection."; GH.curInt = nullptr; { boost::unique_lock un(*LOCPLINT->pim); logNetwork->infoStream() << "Ending current game!"; if(GH.topInt()) GH.topInt()->deactivate(); GH.listInt.clear(); GH.objsToBlit.clear(); GH.statusbar = nullptr; logNetwork->infoStream() << "Removed GUI."; vstd::clear_pointer(const_cast(CGI)->mh); vstd::clear_pointer(gs); logNetwork->infoStream() << "Deleted mapHandler and gameState."; LOCPLINT = nullptr; } playerint.clear(); battleints.clear(); callbacks.clear(); battleCallbacks.clear(); logNetwork->infoStream() << "Deleted playerInts."; logNetwork->infoStream() << "Client stopped."; } void CClient::loadGame( const std::string & fname ) { logNetwork->infoStream() <<"Loading procedure started!"; CServerHandler sh; sh.startServer(); CStopWatch tmh; try { std::string clientSaveName = *CResourceHandler::get()->getResourceName(ResourceID(fname, EResType::CLIENT_SAVEGAME)); std::string controlServerSaveName; if (CResourceHandler::get()->existsResource(ResourceID(fname, EResType::SERVER_SAVEGAME))) { controlServerSaveName = *CResourceHandler::get()->getResourceName(ResourceID(fname, EResType::SERVER_SAVEGAME)); } else// create entry for server savegame. Triggered if save was made after launch and not yet present in res handler { controlServerSaveName = clientSaveName.substr(0, clientSaveName.find_last_of(".")) + ".vsgm1"; CResourceHandler::get()->createResource(controlServerSaveName, true); } if(clientSaveName.empty()) throw std::runtime_error("Cannot open client part of " + fname); if(controlServerSaveName.empty()) throw std::runtime_error("Cannot open server part of " + fname); unique_ptr loader; { CLoadIntegrityValidator checkingLoader(clientSaveName, controlServerSaveName, minSupportedVersion); loadCommonState(checkingLoader); loader = checkingLoader.decay(); } logNetwork->infoStream() << "Loaded common part of save " << tmh.getDiff(); const_cast(CGI)->mh = new CMapHandler(); const_cast(CGI)->mh->map = gs->map; pathInfo = make_unique(getMapSize()); CGI->mh->init(); logNetwork->infoStream() <<"Initing maphandler: "<> *this; logNetwork->infoStream() << "Loaded client part of save " << tmh.getDiff(); } catch(std::exception &e) { logGlobal->errorStream() << "Cannot load game " << fname << ". Error: " << e.what(); throw; //obviously we cannot continue here } serv = sh.connectToServer(); serv->addStdVecItems(gs); tmh.update(); ui8 pom8; *serv << ui8(3) << ui8(1); //load game; one client *serv << fname; *serv >> pom8; if(pom8) throw std::runtime_error("Server cannot open the savegame!"); else logNetwork->infoStream() << "Server opened savegame properly."; *serv << ui32(gs->scenarioOps->playerInfos.size()+1); //number of players + neutral for(auto & elem : gs->scenarioOps->playerInfos) { *serv << ui8(elem.first.getNum()); //players } *serv << ui8(PlayerColor::NEUTRAL.getNum()); logNetwork->infoStream() <<"Sent info to server: "<enableStackSendingByID(); serv->disableSmartPointerSerialization(); // logGlobal->traceStream() << "Objects:"; // for(int i = 0; i < gs->map->objects.size(); i++) // { // auto o = gs->map->objects[i]; // if(o) // logGlobal->traceStream() << boost::format("\tindex=%5d, id=%5d; address=%5d, pos=%s, name=%s") % i % o->id % (int)o.get() % o->pos % o->getHoverText(); // else // logGlobal->traceStream() << boost::format("\tindex=%5d --- nullptr") % i; // } } void CClient::newGame( CConnection *con, StartInfo *si ) { enum {SINGLE, HOST, GUEST} networkMode = SINGLE; if (con == nullptr) { CServerHandler sh; serv = sh.connectToServer(); } else { serv = con; networkMode = (con->connectionID == 1) ? HOST : GUEST; } CConnection &c = *serv; //////////////////////////////////////////////////// logNetwork->infoStream() <<"\tWill send info to server..."; CStopWatch tmh; if(networkMode == SINGLE) { ui8 pom8; c << ui8(2) << ui8(1); //new game; one client c << *si; c >> pom8; if(pom8) throw std::runtime_error("Server cannot open the map!"); } c >> si; logNetwork->infoStream() <<"\tSending/Getting info to/from the server: "<infoStream() <<"\tCreating gamestate: "<scenarioOps = si; gs->init(si); logNetwork->infoStream() <<"Initializing GameState (together): "< myPlayers; for(auto & elem : gs->scenarioOps->playerInfos) { if((networkMode == SINGLE) //single - one client has all player || (networkMode != SINGLE && serv->connectionID == elem.second.playerID) //multi - client has only "its players" || (networkMode == HOST && elem.second.playerID == PlayerSettings::PLAYER_AI))//multi - host has all AI players { myPlayers.insert(elem.first); //add player } } if(networkMode != GUEST) myPlayers.insert(PlayerColor::NEUTRAL); c << myPlayers; // Init map handler if(gs->map) { const_cast(CGI)->mh = new CMapHandler(); CGI->mh->map = gs->map; logNetwork->infoStream() <<"Creating mapHandler: "<mh->init(); pathInfo = make_unique(getMapSize()); logNetwork->infoStream() <<"Initializing mapHandler (together): "<scenarioOps->playerInfos)//initializing interfaces for players { PlayerColor color = elem.first; gs->currentPlayer = color; if(!vstd::contains(myPlayers, color)) continue; logNetwork->traceStream() << "Preparing interface for player " << color; if(si->mode != StartInfo::DUEL) { if(elem.second.playerID == PlayerSettings::PLAYER_AI) { auto AiToGive = aiNameForPlayer(elem.second, false); logNetwork->infoStream() << boost::format("Player %s will be lead by %s") % color % AiToGive; installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); } else { installNewPlayerInterface(make_shared(color), color); humanPlayers++; } } else { std::string AItoGive = aiNameForPlayer(elem.second, true); installNewBattleInterface(CDynLibHandler::getNewBattleAI(AItoGive), color); } } if(si->mode == StartInfo::DUEL) { if(!gNoGUI) { boost::unique_lock un(*LOCPLINT->pim); auto p = make_shared(PlayerColor::NEUTRAL); p->observerInDuelMode = true; installNewPlayerInterface(p, boost::none); GH.curInt = p.get(); } battleStarted(gs->curB); } else { loadNeutralBattleAI(); } serv->addStdVecItems(gs); hotSeat = (humanPlayers > 1); // std::vector scriptModules; // CFileUtility::getFilesWithExt(scriptModules, LIB_DIR "/scripting", "." LIB_EXT); // for(FileInfo &m : scriptModules) // { // CScriptingModule * nm = CDynLibHandler::getNewScriptingModule(m.name); // privilagedGameEventReceivers.push_back(nm); // privilagedBattleEventReceivers.push_back(nm); // nm->giveActionCB(this); // nm->giveInfoCB(this); // nm->init(); // // erm = nm; //something tells me that there'll at most one module and it'll be ERM // } } template void CClient::serialize( Handler &h, const int version ) { h & hotSeat; if(h.saving) { ui8 players = playerint.size(); h & players; for(auto i = playerint.begin(); i != playerint.end(); i++) { LOG_TRACE_PARAMS(logGlobal, "Saving player %s interface", i->first); assert(i->first == i->second->playerID); h & i->first & i->second->dllName & i->second->human; i->second->saveGame(dynamic_cast&>(h), version); //evil cast that i still like better than sfinae-magic. If I had a "static if"... } } else { ui8 players = 0; //fix for uninitialized warning h & players; for(int i=0; i < players; i++) { std::string dllname; PlayerColor pid; bool isHuman = false; h & pid & dllname & isHuman; LOG_TRACE_PARAMS(logGlobal, "Loading player %s interface", pid); shared_ptr nInt; if(dllname.length()) { if(pid == PlayerColor::NEUTRAL) { installNewBattleInterface(CDynLibHandler::getNewBattleAI(dllname), pid); //TODO? consider serialization continue; } else { assert(!isHuman); nInt = CDynLibHandler::getNewAI(dllname); } } else { assert(isHuman); nInt = make_shared(pid); } nInt->dllName = dllname; nInt->human = isHuman; nInt->playerID = pid; installNewPlayerInterface(nInt, pid); nInt->loadGame(dynamic_cast&>(h), version); //another evil cast, check above } if(!vstd::contains(battleints, PlayerColor::NEUTRAL)) loadNeutralBattleAI(); } } void CClient::handlePack( CPack * pack ) { CBaseForCLApply *apply = applier->apps[typeList.getTypeID(pack)]; //find the applier if(apply) { boost::unique_lock guiLock(*LOCPLINT->pim); apply->applyOnClBefore(this,pack); logNetwork->traceStream() << "\tMade first apply on cl"; gs->apply(pack); logNetwork->traceStream() << "\tApplied on gs"; apply->applyOnClAfter(this,pack); logNetwork->traceStream() << "\tMade second apply on cl"; } else { logNetwork->errorStream() << "Message cannot be applied, cannot find applier! TypeID " << typeList.getTypeID(pack); } delete pack; } void CClient::updatePaths() { //TODO? lazy evaluation? paths now can get recalculated multiple times upon various game events const CGHeroInstance *h = getSelectedHero(); if (h)//if we have selected hero... calculatePaths(h); } void CClient::finishCampaign( shared_ptr camp ) { } void CClient::proposeNextMission(shared_ptr camp) { GH.pushInt(new CBonusSelection(camp)); } void CClient::stopConnection() { terminate = true; if (serv) //request closing connection { logNetwork->infoStream() << "Connection has been requested to be closed."; boost::unique_lock(*serv->wmx); CloseServer close_server; sendRequest(&close_server, PlayerColor::NEUTRAL); logNetwork->infoStream() << "Sent closing signal to the server"; } if(connectionHandler)//end connection handler { if(connectionHandler->get_id() != boost::this_thread::get_id()) connectionHandler->join(); logNetwork->infoStream() << "Connection handler thread joined"; delete connectionHandler; connectionHandler = nullptr; } if (serv) //and delete connection { serv->close(); delete serv; serv = nullptr; logNetwork->warnStream() << "Our socket has been closed."; } } void CClient::battleStarted(const BattleInfo * info) { for(auto &battleCb : battleCallbacks) { if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) || battleCb.first >= PlayerColor::PLAYER_LIMIT) { battleCb.second->setBattle(info); } } // for(ui8 side : info->sides) // if(battleCallbacks.count(side)) // battleCallbacks[side]->setBattle(info); shared_ptr att, def; auto &leftSide = info->sides[0], &rightSide = info->sides[1]; //If quick combat is not, do not prepare interfaces for battleint if(!settings["adventure"]["quickCombat"].Bool()) { if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) att = std::dynamic_pointer_cast( playerint[leftSide.color] ); if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) def = std::dynamic_pointer_cast( playerint[rightSide.color] ); } if(!gNoGUI && (!!att || !!def || gs->scenarioOps->mode == StartInfo::DUEL)) { boost::unique_lock un(*LOCPLINT->pim); auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, Rect((screen->w - 800)/2, (screen->h - 600)/2, 800, 600), att, def); GH.pushInt(bi); } auto callBattleStart = [&](PlayerColor color, ui8 side){ if(vstd::contains(battleints, color)) battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side); }; callBattleStart(leftSide.color, 0); callBattleStart(rightSide.color, 1); callBattleStart(PlayerColor::UNFLAGGABLE, 1); if(info->tacticDistance && vstd::contains(battleints,info->sides[info->tacticsSide].color)) { boost::thread(&CClient::commenceTacticPhaseForInt, this, battleints[info->sides[info->tacticsSide].color]); } } void CClient::battleFinished() { for(auto & side : gs->curB->sides) if(battleCallbacks.count(side.color)) battleCallbacks[side.color]->setBattle(nullptr); } void CClient::loadNeutralBattleAI() { installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); } void CClient::commitPackage( CPackForClient *pack ) { CommitPackage cp; cp.freePack = false; cp.packToCommit = pack; sendRequest(&cp, PlayerColor::NEUTRAL); } PlayerColor CClient::getLocalPlayer() const { if(LOCPLINT) return LOCPLINT->playerID; return getCurrentPlayer(); } void CClient::calculatePaths(const CGHeroInstance *h) { assert(h); boost::unique_lock pathLock(pathMx); gs->calculatePaths(h, *pathInfo); } void CClient::commenceTacticPhaseForInt(shared_ptr battleInt) { setThreadName("CClient::commenceTacticPhaseForInt"); try { battleInt->yourTacticPhase(gs->curB->tacticDistance); if(gs && !!gs->curB && gs->curB->tacticDistance) //while awaiting for end of tactics phase, many things can happen (end of battle... or game) { MakeAction ma(BattleAction::makeEndOFTacticPhase(gs->curB->playerToSide(battleInt->playerID))); sendRequest(&ma, battleInt->playerID); } } HANDLE_EXCEPTION } void CClient::invalidatePaths(const CGHeroInstance *h /*= nullptr*/) { if(!h || pathInfo->hero == h) pathInfo->isValid = false; } int CClient::sendRequest(const CPack *request, PlayerColor player) { static ui32 requestCounter = 0; ui32 requestID = requestCounter++; logNetwork->traceStream() << boost::format("Sending a request \"%s\". It'll have an ID=%d.") % typeid(*request).name() % requestID; waitingRequest.pushBack(requestID); serv->sendPackToServer(*request, player, requestID); if(vstd::contains(playerint, player)) playerint[player]->requestSent(dynamic_cast(request), requestID); return requestID; } void CClient::campaignMapFinished( shared_ptr camp ) { endGame(false); GH.curInt = CGPreGame::create(); auto & epilogue = camp->camp->scenarios[camp->mapsConquered.back()].epilog; auto finisher = [=]() { if(camp->mapsRemaining.size()) proposeNextMission(camp); else finishCampaign(camp); }; if(epilogue.hasPrologEpilog) { GH.pushInt(new CPrologEpilogVideo(epilogue, finisher)); } else { finisher(); } } void CClient::installNewPlayerInterface(shared_ptr gameInterface, boost::optional color) { boost::unique_lock un(*LOCPLINT->pim); PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE); if(!color) privilagedGameEventReceivers.push_back(gameInterface); playerint[colorUsed] = gameInterface; logGlobal->traceStream() << boost::format("\tInitializing the interface for player %s") % colorUsed; auto cb = make_shared(gs, color, this); callbacks[colorUsed] = cb; battleCallbacks[colorUsed] = cb; gameInterface->init(cb); installNewBattleInterface(gameInterface, color, false); } void CClient::installNewBattleInterface(shared_ptr battleInterface, boost::optional color, bool needCallback /*= true*/) { boost::unique_lock un(*LOCPLINT->pim); PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE); if(!color) privilagedBattleEventReceivers.push_back(battleInterface); battleints[colorUsed] = battleInterface; if(needCallback) { logGlobal->traceStream() << boost::format("\tInitializing the battle interface for player %s") % *color; auto cbc = make_shared(gs, color, this); battleCallbacks[colorUsed] = cbc; battleInterface->init(cbc); } } std::string CClient::aiNameForPlayer(const PlayerSettings &ps, bool battleAI) { if(ps.name.size()) { std::string filename = VCMIDirs::get().libraryPath() + "/AI/" + VCMIDirs::get().libraryName(ps.name); if(boost::filesystem::exists(filename)) return ps.name; } const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; std::string goodAI = battleAI ? settings["server"]["neutralAI"].String() : settings["server"]["playerAI"].String(); std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; //TODO what about human players if(battleints.size() >= sensibleAILimit) return badAI; return goodAI; } template void CClient::serialize( CISer &h, const int version ); template void CClient::serialize( COSer &h, const int version ); void CServerHandler::startServer() { th.update(); serverThread = new boost::thread(&CServerHandler::callServer, this); //runs server executable; if(verbose) logNetwork->infoStream() << "Setting up thread calling server: " << th.getDiff(); } void CServerHandler::waitForServer() { if(!serverThread) startServer(); th.update(); intpr::scoped_lock slock(shared->sr->mutex); while(!shared->sr->ready) { shared->sr->cond.wait(slock); } if(verbose) logNetwork->infoStream() << "Waiting for server: " << th.getDiff(); } CConnection * CServerHandler::connectToServer() { if(!shared->sr->ready) waitForServer(); th.update(); //put breakpoint here to attach to server before it does something stupid CConnection *ret = justConnectToServer(settings["server"]["server"].String(), port); if(verbose) logNetwork->infoStream()<<"\tConnecting to the server: "<(settings["server"]["port"].Float()); verbose = true; boost::interprocess::shared_memory_object::remove("vcmi_memory"); //if the application has previously crashed, the memory may not have been removed. to avoid problems - try to destroy it try { shared = new SharedMem(); } HANDLE_EXCEPTIONC(logNetwork->errorStream() << "Cannot open interprocess memory: ";) } CServerHandler::~CServerHandler() { delete shared; delete serverThread; //detaches, not kills thread } void CServerHandler::callServer() { setThreadName("CServerHandler::callServer"); std::string logName = VCMIDirs::get().userCachePath() + "/server_log.txt"; std::string comm = VCMIDirs::get().serverPath() + " --port=" + port + " > " + logName; int result = std::system(comm.c_str()); if (result == 0) logNetwork->infoStream() << "Server closed correctly"; else { logNetwork->errorStream() << "Error: server failed to close correctly or crashed!"; logNetwork->errorStream() << "Check " << logName << " for more info"; exit(1);// exit in case of error. Othervice without working server VCMI will hang } } CConnection * CServerHandler::justConnectToServer(const std::string &host, const std::string &port) { CConnection *ret = nullptr; while(!ret) { try { logNetwork->infoStream() << "Establishing connection..."; ret = new CConnection( host.size() ? host : settings["server"]["server"].String(), port.size() ? port : boost::lexical_cast(settings["server"]["port"].Float()), NAME); } catch(...) { logNetwork->errorStream() << "\nCannot establish connection! Retrying within 2 seconds"; SDL_Delay(2000); } } return ret; }