2017-07-13 10:26:03 +02:00
|
|
|
/*
|
|
|
|
* 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
|
|
|
|
*
|
|
|
|
*/
|
2011-12-14 00:23:17 +03:00
|
|
|
#include "StdInc.h"
|
2014-07-15 10:14:49 +03:00
|
|
|
#include "Client.h"
|
|
|
|
|
|
|
|
#include <SDL.h>
|
2011-12-14 00:23:17 +03:00
|
|
|
|
2010-12-20 23:22:53 +02:00
|
|
|
#include "CMusicHandler.h"
|
2013-04-07 13:48:07 +03:00
|
|
|
#include "../lib/mapping/CCampaignHandler.h"
|
2008-07-25 20:28:28 +03:00
|
|
|
#include "../CCallback.h"
|
2011-12-14 00:23:17 +03:00
|
|
|
#include "../lib/CConsoleHandler.h"
|
2009-05-20 13:08:56 +03:00
|
|
|
#include "CGameInfo.h"
|
|
|
|
#include "../lib/CGameState.h"
|
|
|
|
#include "CPlayerInterface.h"
|
2011-12-14 00:23:17 +03:00
|
|
|
#include "../lib/StartInfo.h"
|
2017-06-24 16:42:05 +02:00
|
|
|
#include "../lib/battle/BattleInfo.h"
|
2012-08-10 16:07:53 +03:00
|
|
|
#include "../lib/CModHandler.h"
|
2010-12-20 23:22:53 +02:00
|
|
|
#include "../lib/CArtHandler.h"
|
|
|
|
#include "../lib/CGeneralTextHandler.h"
|
|
|
|
#include "../lib/CHeroHandler.h"
|
|
|
|
#include "../lib/CTownHandler.h"
|
|
|
|
#include "../lib/CBuildingHandler.h"
|
2015-02-02 10:25:26 +02:00
|
|
|
#include "../lib/spells/CSpellHandler.h"
|
2016-09-10 02:32:40 +02:00
|
|
|
#include "../lib/serializer/CTypeList.h"
|
|
|
|
#include "../lib/serializer/Connection.h"
|
|
|
|
#include "../lib/serializer/CLoadIntegrityValidator.h"
|
2008-07-26 16:57:32 +03:00
|
|
|
#include "../lib/NetPacks.h"
|
2008-11-09 00:29:19 +02:00
|
|
|
#include "../lib/VCMI_Lib.h"
|
2011-11-01 15:58:01 +03:00
|
|
|
#include "../lib/VCMIDirs.h"
|
2013-04-07 13:48:07 +03:00
|
|
|
#include "../lib/mapping/CMap.h"
|
2017-07-20 06:08:49 +02:00
|
|
|
#include "../lib/mapping/CMapService.h"
|
2011-09-01 04:40:46 +03:00
|
|
|
#include "../lib/JsonNode.h"
|
2010-04-06 16:19:54 +03:00
|
|
|
#include "mapHandler.h"
|
2012-09-29 13:59:43 +03:00
|
|
|
#include "../lib/CConfigHandler.h"
|
2018-01-05 19:21:07 +02:00
|
|
|
#include "mainmenu/CMainMenu.h"
|
|
|
|
#include "mainmenu/CCampaignScreen.h"
|
|
|
|
#include "lobby/CBonusSelection.h"
|
2013-04-07 14:52:07 +03:00
|
|
|
#include "battle/CBattleInterface.h"
|
2011-12-14 00:23:17 +03:00
|
|
|
#include "../lib/CThreadHelper.h"
|
2011-06-20 14:41:04 +03:00
|
|
|
#include "../lib/CScriptingModule.h"
|
2014-02-24 22:57:33 +03:00
|
|
|
#include "../lib/registerTypes/RegisterTypes.h"
|
2013-04-07 14:52:07 +03:00
|
|
|
#include "gui/CGuiHandler.h"
|
2013-06-17 18:45:55 +03:00
|
|
|
#include "CMT.h"
|
2018-01-05 19:21:07 +02:00
|
|
|
#include "CServerHandler.h"
|
2011-05-10 01:20:47 +03:00
|
|
|
|
2017-06-04 14:59:11 +02:00
|
|
|
#ifdef VCMI_ANDROID
|
2017-05-25 19:57:20 +02:00
|
|
|
#include "lib/CAndroidVMHelper.h"
|
2014-02-20 21:53:18 +03:00
|
|
|
#endif
|
2008-07-28 15:44:08 +03:00
|
|
|
|
2017-05-25 19:57:20 +02:00
|
|
|
#ifdef VCMI_ANDROID
|
|
|
|
std::atomic_bool androidTestServerReadyFlag;
|
|
|
|
#endif
|
|
|
|
|
2017-07-12 16:02:25 +02:00
|
|
|
ThreadSafeVector<int> CClient::waitingRequest;
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
template<typename T> class CApplyOnCL;
|
2010-09-03 21:42:54 +03:00
|
|
|
|
2009-03-07 00:25:19 +02:00
|
|
|
class CBaseForCLApply
|
|
|
|
{
|
|
|
|
public:
|
2018-01-05 19:21:07 +02:00
|
|
|
virtual void applyOnClAfter(CClient * cl, void * pack) const =0;
|
|
|
|
virtual void applyOnClBefore(CClient * cl, void * pack) const =0;
|
2010-01-02 03:48:44 +02:00
|
|
|
virtual ~CBaseForCLApply(){}
|
2010-09-03 21:42:54 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
template<typename U> static CBaseForCLApply * getApplier(const U * t = nullptr)
|
2010-09-03 21:42:54 +03:00
|
|
|
{
|
2017-07-16 11:58:05 +02:00
|
|
|
return new CApplyOnCL<U>();
|
2010-09-03 21:42:54 +03:00
|
|
|
}
|
2009-03-07 00:25:19 +02:00
|
|
|
};
|
2010-09-03 21:42:54 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
template<typename T> class CApplyOnCL : public CBaseForCLApply
|
2009-03-07 00:25:19 +02:00
|
|
|
{
|
|
|
|
public:
|
2018-01-05 19:21:07 +02:00
|
|
|
void applyOnClAfter(CClient * cl, void * pack) const override
|
2009-03-07 00:25:19 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
T * ptr = static_cast<T *>(pack);
|
2009-03-07 00:25:19 +02:00
|
|
|
ptr->applyCl(cl);
|
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
void applyOnClBefore(CClient * cl, void * pack) const override
|
2009-03-07 00:25:19 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
T * ptr = static_cast<T *>(pack);
|
2009-03-07 00:25:19 +02:00
|
|
|
ptr->applyFirstCl(cl);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
template<> class CApplyOnCL<CPack>: public CBaseForCLApply
|
2014-02-19 04:04:27 +03:00
|
|
|
{
|
|
|
|
public:
|
2018-01-05 19:21:07 +02:00
|
|
|
void applyOnClAfter(CClient * cl, void * pack) const override
|
2014-02-19 04:04:27 +03:00
|
|
|
{
|
2017-08-10 18:39:27 +02:00
|
|
|
logGlobal->error("Cannot apply on CL plain CPack!");
|
2014-02-19 04:04:27 +03:00
|
|
|
assert(0);
|
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
void applyOnClBefore(CClient * cl, void * pack) const override
|
2014-02-19 04:04:27 +03:00
|
|
|
{
|
2017-08-10 18:39:27 +02:00
|
|
|
logGlobal->error("Cannot apply on CL plain CPack!");
|
2014-02-19 04:04:27 +03:00
|
|
|
assert(0);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
CClient::CClient()
|
2008-07-25 20:28:28 +03:00
|
|
|
{
|
2017-07-12 16:02:25 +02:00
|
|
|
waitingRequest.clear();
|
2013-05-09 14:09:23 +03:00
|
|
|
pathInfo = nullptr;
|
2018-01-05 19:21:07 +02:00
|
|
|
applier = std::make_shared<CApplier<CBaseForCLApply>>();
|
2014-02-24 22:57:33 +03:00
|
|
|
registerTypesClientPacks1(*applier);
|
|
|
|
registerTypesClientPacks2(*applier);
|
2009-02-01 16:11:41 +02:00
|
|
|
IObjectInterface::cb = this;
|
2013-05-09 14:09:23 +03:00
|
|
|
gs = nullptr;
|
|
|
|
erm = nullptr;
|
2009-01-30 23:28:02 +02:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::newGame()
|
2009-01-30 23:28:02 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
CSH->th->update();
|
|
|
|
CMapService mapService;
|
|
|
|
gs = new CGameState();
|
|
|
|
logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff());
|
|
|
|
gs->init(&mapService, CSH->si.get(), settings["general"]["saveRandomMaps"].Bool());
|
|
|
|
logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff());
|
2010-09-03 21:42:54 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
initMapHandler();
|
|
|
|
initPlayerInterfaces();
|
2008-07-25 20:28:28 +03:00
|
|
|
}
|
2010-09-03 21:42:54 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::loadGame()
|
2008-07-25 20:28:28 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
logNetwork->info("Loading procedure started!");
|
2010-09-03 21:42:54 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
std::unique_ptr<CLoadFile> loader;
|
2008-08-30 00:41:32 +03:00
|
|
|
try
|
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
boost::filesystem::path clientSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(CSH->si->mapname, EResType::CLIENT_SAVEGAME));
|
|
|
|
boost::filesystem::path controlServerSaveName;
|
|
|
|
|
|
|
|
if(CResourceHandler::get("local")->existsResource(ResourceID(CSH->si->mapname, EResType::SERVER_SAVEGAME)))
|
2015-10-08 07:15:29 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
controlServerSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(CSH->si->mapname, EResType::SERVER_SAVEGAME));
|
2015-10-08 07:15:29 +02:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
else // create entry for server savegame. Triggered if save was made after launch and not yet present in res handler
|
|
|
|
{
|
|
|
|
controlServerSaveName = boost::filesystem::path(clientSaveName).replace_extension(".vsgm1");
|
|
|
|
CResourceHandler::get("local")->createResource(controlServerSaveName.string(), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(clientSaveName.empty())
|
|
|
|
throw std::runtime_error("Cannot open client part of " + CSH->si->mapname);
|
|
|
|
if(controlServerSaveName.empty() || !boost::filesystem::exists(controlServerSaveName))
|
|
|
|
throw std::runtime_error("Cannot open server part of " + CSH->si->mapname);
|
|
|
|
|
|
|
|
{
|
|
|
|
CLoadIntegrityValidator checkingLoader(clientSaveName, controlServerSaveName, MINIMAL_SERIALIZATION_VERSION);
|
|
|
|
loadCommonState(checkingLoader);
|
|
|
|
loader = checkingLoader.decay();
|
|
|
|
}
|
|
|
|
|
2012-02-20 00:03:43 +03:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
catch(std::exception & e)
|
2012-02-20 00:03:43 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
logGlobal->error("Cannot load game %s. Error: %s", CSH->si->mapname, e.what());
|
|
|
|
throw; //obviously we cannot continue here
|
2012-02-20 00:03:43 +03:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
logNetwork->trace("Loaded common part of save %d ms", CSH->th->getDiff());
|
|
|
|
|
|
|
|
gs->updateOnLoad(CSH->si.get());
|
|
|
|
initMapHandler();
|
|
|
|
serialize(loader->serializer, loader->serializer.fileVersion);
|
|
|
|
initPlayerInterfaces();
|
|
|
|
}
|
|
|
|
|
|
|
|
void CClient::serialize(BinarySerializer & h, const int version)
|
|
|
|
{
|
|
|
|
assert(h.saving);
|
|
|
|
ui8 players = playerint.size();
|
|
|
|
h & players;
|
|
|
|
|
|
|
|
for(auto i = playerint.begin(); i != playerint.end(); i++)
|
2015-02-14 21:42:47 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
logGlobal->trace("Saving player %s interface", i->first);
|
|
|
|
assert(i->first == i->second->playerID);
|
|
|
|
h & i->first;
|
|
|
|
h & i->second->dllName;
|
|
|
|
h & i->second->human;
|
|
|
|
i->second->saveGame(h, version);
|
2015-02-14 21:42:47 +02:00
|
|
|
}
|
2008-08-04 18:56:36 +03:00
|
|
|
}
|
2009-12-28 06:08:24 +02:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::serialize(BinaryDeserializer & h, const int version)
|
2008-07-25 20:28:28 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
assert(!h.saving);
|
|
|
|
if(version < 787)
|
2008-07-25 20:28:28 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
bool hotSeat = false;
|
|
|
|
h & hotSeat;
|
|
|
|
}
|
|
|
|
|
|
|
|
ui8 players = 0;
|
|
|
|
h & players;
|
|
|
|
|
|
|
|
for(int i = 0; i < players; i++)
|
|
|
|
{
|
|
|
|
std::string dllname;
|
|
|
|
PlayerColor pid;
|
|
|
|
bool isHuman = false;
|
|
|
|
|
|
|
|
h & pid;
|
|
|
|
h & dllname;
|
|
|
|
h & isHuman;
|
|
|
|
assert(dllname.length() == 0 || !isHuman);
|
|
|
|
if(pid == PlayerColor::NEUTRAL)
|
2009-03-07 17:54:12 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
logGlobal->trace("Neutral battle interfaces are not serialized.");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
logGlobal->trace("Loading player %s interface", pid);
|
|
|
|
std::shared_ptr<CGameInterface> nInt;
|
|
|
|
if(dllname.length())
|
|
|
|
nInt = CDynLibHandler::getNewAI(dllname);
|
|
|
|
else
|
|
|
|
nInt = std::make_shared<CPlayerInterface>(pid);
|
2016-01-16 17:36:16 +02:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
nInt->dllName = dllname;
|
|
|
|
nInt->human = isHuman;
|
|
|
|
nInt->playerID = pid;
|
2009-11-01 03:15:16 +02:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
nInt->loadGame(h, version);
|
|
|
|
|
|
|
|
// Client no longer handle this player at all
|
|
|
|
if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid))
|
|
|
|
{
|
|
|
|
logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid);
|
2009-03-07 17:54:12 +02:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
else if(isHuman && !vstd::contains(CSH->getHumanColors(), pid))
|
2010-01-29 22:52:45 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
logGlobal->trace("Player %s is no longer controlled by human. Destroying interface", pid);
|
|
|
|
}
|
|
|
|
else if(!isHuman && vstd::contains(CSH->getHumanColors(), pid))
|
|
|
|
{
|
|
|
|
logGlobal->trace("Player %s is no longer controlled by AI. Destroying interface", pid);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
installNewPlayerInterface(nInt, pid);
|
|
|
|
continue;
|
2010-01-29 22:52:45 +02:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
nInt.reset();
|
2010-01-29 22:52:45 +02:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff());
|
2008-08-04 12:05:52 +03:00
|
|
|
}
|
|
|
|
|
2008-11-16 03:06:15 +02:00
|
|
|
void CClient::save(const std::string & fname)
|
|
|
|
{
|
2009-02-11 19:03:30 +02:00
|
|
|
if(gs->curB)
|
|
|
|
{
|
2017-08-10 18:39:27 +02:00
|
|
|
logNetwork->error("Game cannot be saved during battle!");
|
2009-02-11 19:03:30 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2011-06-11 07:54:41 +03:00
|
|
|
SaveGame save_game(fname);
|
2018-01-05 19:21:07 +02:00
|
|
|
sendRequest(&save_game, PlayerColor::NEUTRAL);
|
2008-11-16 03:06:15 +02:00
|
|
|
}
|
2008-12-27 03:01:59 +02:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::endGame()
|
2009-01-11 00:08:18 +02:00
|
|
|
{
|
2012-02-22 16:41:27 +03:00
|
|
|
//suggest interfaces to finish their stuff (AI should interrupt any bg working threads)
|
2018-01-05 19:21:07 +02:00
|
|
|
for(auto & i : playerint)
|
2012-02-22 16:41:27 +03:00
|
|
|
i.second->finish();
|
|
|
|
|
2013-06-26 14:18:27 +03:00
|
|
|
GH.curInt = nullptr;
|
2012-02-20 00:03:43 +03:00
|
|
|
{
|
2016-11-25 21:12:22 +02:00
|
|
|
boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
|
2017-08-10 18:39:27 +02:00
|
|
|
logNetwork->info("Ending current game!");
|
2012-02-20 00:03:43 +03:00
|
|
|
if(GH.topInt())
|
2016-01-20 12:02:52 +02:00
|
|
|
{
|
2012-02-20 00:03:43 +03:00
|
|
|
GH.topInt()->deactivate();
|
2016-01-20 12:02:52 +02:00
|
|
|
}
|
2012-02-20 00:03:43 +03:00
|
|
|
GH.listInt.clear();
|
|
|
|
GH.objsToBlit.clear();
|
2013-06-26 14:18:27 +03:00
|
|
|
GH.statusbar = nullptr;
|
2017-08-10 18:39:27 +02:00
|
|
|
logNetwork->info("Removed GUI.");
|
2012-02-20 00:03:43 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
vstd::clear_pointer(const_cast<CGameInfo *>(CGI)->mh);
|
2013-05-09 14:09:23 +03:00
|
|
|
vstd::clear_pointer(gs);
|
2012-02-20 00:03:43 +03:00
|
|
|
|
2017-08-10 18:39:27 +02:00
|
|
|
logNetwork->info("Deleted mapHandler and gameState.");
|
2013-06-26 14:18:27 +03:00
|
|
|
LOCPLINT = nullptr;
|
2012-02-20 00:03:43 +03:00
|
|
|
}
|
2009-06-23 11:14:49 +03:00
|
|
|
|
2013-06-23 00:47:51 +03:00
|
|
|
playerint.clear();
|
2013-12-06 22:44:11 +03:00
|
|
|
battleints.clear();
|
2011-09-06 09:00:32 +03:00
|
|
|
callbacks.clear();
|
2013-06-22 17:47:20 +03:00
|
|
|
battleCallbacks.clear();
|
2017-08-10 18:39:27 +02:00
|
|
|
logNetwork->info("Deleted playerInts.");
|
|
|
|
logNetwork->info("Client stopped.");
|
2009-11-01 03:15:16 +02:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::initMapHandler()
|
2014-10-21 15:11:54 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
// TODO: CMapHandler initialization can probably go somewhere else
|
|
|
|
// It's can't be before initialization of interfaces
|
|
|
|
// During loading CPlayerInterface from serialized state it's depend on MH
|
|
|
|
if(!settings["session"]["headless"].Bool())
|
2014-10-21 15:11:54 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
const_cast<CGameInfo *>(CGI)->mh = new CMapHandler();
|
|
|
|
CGI->mh->map = gs->map;
|
|
|
|
logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff());
|
2014-10-21 15:11:54 +03:00
|
|
|
CGI->mh->init();
|
2018-01-05 19:21:07 +02:00
|
|
|
logNetwork->trace("Initializing mapHandler (together): %d ms", CSH->th->getDiff());
|
2016-03-12 03:41:27 +02:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
pathInfo = make_unique<CPathsInfo>(getMapSize());
|
2009-01-11 00:08:18 +02:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::initPlayerInterfaces()
|
2009-01-11 00:08:18 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
for(auto & elem : CSH->si->playerInfos)
|
2013-02-19 02:10:46 +03:00
|
|
|
{
|
2013-06-29 16:05:48 +03:00
|
|
|
PlayerColor color = elem.first;
|
2018-01-05 19:21:07 +02:00
|
|
|
if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if(vstd::contains(playerint, color))
|
2010-10-24 14:35:14 +03:00
|
|
|
continue;
|
|
|
|
|
2017-08-12 13:36:04 +02:00
|
|
|
logNetwork->trace("Preparing interface for player %s", color.getStr());
|
2018-01-05 19:21:07 +02:00
|
|
|
if(elem.second.isControlledByAI())
|
2010-02-20 15:24:38 +02:00
|
|
|
{
|
2017-07-01 15:30:13 +02:00
|
|
|
auto AiToGive = aiNameForPlayer(elem.second, false);
|
|
|
|
logNetwork->info("Player %s will be lead by %s", color, AiToGive);
|
|
|
|
installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color);
|
2009-01-11 00:08:18 +02:00
|
|
|
}
|
2010-12-22 22:14:40 +02:00
|
|
|
else
|
2010-02-20 15:24:38 +02:00
|
|
|
{
|
2017-07-01 15:30:13 +02:00
|
|
|
installNewPlayerInterface(std::make_shared<CPlayerInterface>(color), color);
|
2009-01-11 00:08:18 +02:00
|
|
|
}
|
|
|
|
}
|
2010-02-20 15:24:38 +02:00
|
|
|
|
2017-07-01 15:30:13 +02:00
|
|
|
if(settings["session"]["spectate"].Bool())
|
2010-12-25 03:43:40 +02:00
|
|
|
{
|
2017-07-01 15:30:13 +02:00
|
|
|
installNewPlayerInterface(std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true);
|
2010-12-25 03:43:40 +02:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
|
|
|
|
if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL))
|
|
|
|
installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL);
|
|
|
|
|
|
|
|
logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff());
|
2009-01-11 00:08:18 +02:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI)
|
2009-03-28 20:46:20 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
if(ps.name.size())
|
2009-03-28 20:46:20 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name);
|
|
|
|
if(boost::filesystem::exists(aiPath))
|
|
|
|
return ps.name;
|
2009-03-28 20:46:20 +02:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
|
|
|
|
return aiNameForPlayer(battleAI);
|
2014-12-21 21:33:20 +02:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
std::string CClient::aiNameForPlayer(bool battleAI)
|
2014-12-21 21:33:20 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
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";
|
2009-07-18 06:13:13 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
//TODO what about human players
|
|
|
|
if(battleints.size() >= sensibleAILimit)
|
|
|
|
return badAI;
|
2011-02-27 21:58:14 +02:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
return goodAI;
|
2009-03-28 20:46:20 +02:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color, bool battlecb)
|
2014-10-21 15:11:54 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
|
|
|
|
PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE);
|
2014-10-21 15:11:54 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
if(!color)
|
|
|
|
privilegedGameEventReceivers.push_back(gameInterface);
|
|
|
|
|
|
|
|
playerint[colorUsed] = gameInterface;
|
|
|
|
|
|
|
|
logGlobal->trace("\tInitializing the interface for player %s", colorUsed);
|
|
|
|
auto cb = std::make_shared<CCallback>(gs, color, this);
|
|
|
|
callbacks[colorUsed] = cb;
|
|
|
|
battleCallbacks[colorUsed] = cb;
|
|
|
|
gameInterface->init(cb);
|
|
|
|
|
|
|
|
installNewBattleInterface(gameInterface, color, battlecb);
|
2014-12-21 21:33:20 +02:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback)
|
2014-12-21 21:33:20 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
|
|
|
|
PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE);
|
2014-10-21 15:11:54 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
if(!color)
|
|
|
|
privilegedBattleEventReceivers.push_back(battleInterface);
|
|
|
|
|
|
|
|
battleints[colorUsed] = battleInterface;
|
2014-10-21 15:11:54 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
if(needCallback)
|
|
|
|
{
|
|
|
|
logGlobal->trace("\tInitializing the battle interface for player %s", *color);
|
|
|
|
auto cbc = std::make_shared<CBattleCallback>(color, this);
|
|
|
|
battleCallbacks[colorUsed] = cbc;
|
|
|
|
battleInterface->init(cbc);
|
2014-10-21 15:11:54 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::handlePack(CPack * pack)
|
2014-10-25 12:04:08 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier
|
2010-01-02 03:48:44 +02:00
|
|
|
if(apply)
|
|
|
|
{
|
2016-11-25 21:12:22 +02:00
|
|
|
boost::unique_lock<boost::recursive_mutex> guiLock(*CPlayerInterface::pim);
|
2016-10-02 22:41:57 +02:00
|
|
|
apply->applyOnClBefore(this, pack);
|
2018-01-05 19:21:07 +02:00
|
|
|
logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name());
|
2010-01-02 03:48:44 +02:00
|
|
|
gs->apply(pack);
|
2018-01-05 19:21:07 +02:00
|
|
|
logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name());
|
2016-10-02 22:41:57 +02:00
|
|
|
apply->applyOnClAfter(this, pack);
|
2018-01-05 19:21:07 +02:00
|
|
|
logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name());
|
2010-01-02 03:48:44 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2016-11-18 16:45:59 +02:00
|
|
|
logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name());
|
2010-01-02 03:48:44 +02:00
|
|
|
}
|
|
|
|
delete pack;
|
|
|
|
}
|
2009-08-04 02:53:18 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::commitPackage(CPackForClient * pack)
|
2010-08-20 16:34:39 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
CommitPackage cp;
|
|
|
|
cp.freePack = false;
|
|
|
|
cp.packToCommit = pack;
|
|
|
|
sendRequest(&cp, PlayerColor::NEUTRAL);
|
2010-08-20 16:34:39 +03:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
|
2010-08-20 16:34:39 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
static ui32 requestCounter = 0;
|
2010-08-20 16:34:39 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
ui32 requestID = requestCounter++;
|
|
|
|
logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID);
|
2011-01-17 18:07:08 +02:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
waitingRequest.pushBack(requestID);
|
|
|
|
request->requestID = requestID;
|
|
|
|
request->player = player;
|
|
|
|
CSH->c->sendPack(request);
|
|
|
|
if(vstd::contains(playerint, player))
|
|
|
|
playerint[player]->requestSent(request, requestID);
|
2017-07-12 18:40:14 +02:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
return requestID;
|
2010-08-20 16:34:39 +03:00
|
|
|
}
|
|
|
|
|
2010-12-23 02:33:48 +02:00
|
|
|
void CClient::battleStarted(const BattleInfo * info)
|
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
for(auto & battleCb : battleCallbacks)
|
2012-08-26 12:07:48 +03:00
|
|
|
{
|
2016-01-16 17:36:16 +02:00
|
|
|
if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; })
|
2018-01-05 19:21:07 +02:00
|
|
|
|| battleCb.first >= PlayerColor::PLAYER_LIMIT)
|
2013-07-22 01:01:29 +03:00
|
|
|
{
|
2012-08-26 12:07:48 +03:00
|
|
|
battleCb.second->setBattle(info);
|
2013-07-22 01:01:29 +03:00
|
|
|
}
|
2012-08-26 12:07:48 +03:00
|
|
|
}
|
2013-06-29 16:05:48 +03:00
|
|
|
// for(ui8 side : info->sides)
|
2012-08-26 12:07:48 +03:00
|
|
|
// if(battleCallbacks.count(side))
|
|
|
|
// battleCallbacks[side]->setBattle(info);
|
|
|
|
|
2015-12-29 04:43:33 +02:00
|
|
|
std::shared_ptr<CPlayerInterface> att, def;
|
2018-01-05 19:21:07 +02:00
|
|
|
auto & leftSide = info->sides[0], & rightSide = info->sides[1];
|
2010-12-23 02:33:48 +02:00
|
|
|
|
2013-06-23 14:25:48 +03:00
|
|
|
//If quick combat is not, do not prepare interfaces for battleint
|
|
|
|
if(!settings["adventure"]["quickCombat"].Bool())
|
|
|
|
{
|
2013-07-22 01:01:29 +03:00
|
|
|
if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human)
|
2018-01-05 19:21:07 +02:00
|
|
|
att = std::dynamic_pointer_cast<CPlayerInterface>(playerint[leftSide.color]);
|
2013-06-23 14:25:48 +03:00
|
|
|
|
2013-07-22 01:01:29 +03:00
|
|
|
if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human)
|
2018-01-05 19:21:07 +02:00
|
|
|
def = std::dynamic_pointer_cast<CPlayerInterface>(playerint[rightSide.color]);
|
2013-06-23 14:25:48 +03:00
|
|
|
}
|
2010-12-23 02:33:48 +02:00
|
|
|
|
2017-06-03 07:25:10 +02:00
|
|
|
if(!settings["session"]["headless"].Bool())
|
2012-02-20 00:03:43 +03:00
|
|
|
{
|
2018-07-25 00:36:48 +02:00
|
|
|
Rect battleIntRect((screen->w - 800)/2, (screen->h - 600)/2, 800, 600);
|
2017-07-01 15:30:13 +02:00
|
|
|
if(!!att || !!def)
|
2017-06-03 07:25:10 +02:00
|
|
|
{
|
|
|
|
boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
|
2018-07-25 00:36:48 +02:00
|
|
|
GH.pushIntT<CBattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, battleIntRect, att, def);
|
2017-06-03 07:25:10 +02:00
|
|
|
}
|
|
|
|
else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
|
|
|
|
{
|
|
|
|
//TODO: This certainly need improvement
|
|
|
|
auto spectratorInt = std::dynamic_pointer_cast<CPlayerInterface>(playerint[PlayerColor::SPECTATOR]);
|
|
|
|
spectratorInt->cb->setBattle(info);
|
|
|
|
boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
|
2018-07-25 00:36:48 +02:00
|
|
|
GH.pushIntT<CBattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, battleIntRect, att, def, spectratorInt);
|
2017-06-03 07:25:10 +02:00
|
|
|
}
|
2012-02-20 00:03:43 +03:00
|
|
|
}
|
2010-12-23 02:33:48 +02:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
auto callBattleStart = [&](PlayerColor color, ui8 side)
|
|
|
|
{
|
2013-07-22 01:01:29 +03:00
|
|
|
if(vstd::contains(battleints, color))
|
|
|
|
battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side);
|
|
|
|
};
|
|
|
|
|
|
|
|
callBattleStart(leftSide.color, 0);
|
2013-07-22 19:23:23 +03:00
|
|
|
callBattleStart(rightSide.color, 1);
|
2013-07-22 01:01:29 +03:00
|
|
|
callBattleStart(PlayerColor::UNFLAGGABLE, 1);
|
2017-06-03 07:25:10 +02:00
|
|
|
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
|
|
|
|
callBattleStart(PlayerColor::SPECTATOR, 1);
|
2011-08-25 18:24:37 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
if(info->tacticDistance && vstd::contains(battleints, info->sides[info->tacticsSide].color))
|
2011-08-25 18:24:37 +03:00
|
|
|
{
|
2013-07-22 01:01:29 +03:00
|
|
|
boost::thread(&CClient::commenceTacticPhaseForInt, this, battleints[info->sides[info->tacticsSide].color]);
|
2011-08-25 18:24:37 +03:00
|
|
|
}
|
2010-12-23 02:33:48 +02:00
|
|
|
}
|
|
|
|
|
2015-12-29 04:43:33 +02:00
|
|
|
void CClient::commenceTacticPhaseForInt(std::shared_ptr<CBattleGameInterface> battleInt)
|
2011-08-25 18:24:37 +03:00
|
|
|
{
|
2012-06-27 23:44:01 +03:00
|
|
|
setThreadName("CClient::commenceTacticPhaseForInt");
|
2011-08-25 18:24:37 +03:00
|
|
|
try
|
|
|
|
{
|
|
|
|
battleInt->yourTacticPhase(gs->curB->tacticDistance);
|
2011-08-26 00:08:53 +03:00
|
|
|
if(gs && !!gs->curB && gs->curB->tacticDistance) //while awaiting for end of tactics phase, many things can happen (end of battle... or game)
|
|
|
|
{
|
2017-07-01 10:34:00 +02:00
|
|
|
MakeAction ma(BattleAction::makeEndOFTacticPhase(gs->curB->playerToSide(battleInt->playerID).get()));
|
2012-03-26 01:46:14 +03:00
|
|
|
sendRequest(&ma, battleInt->playerID);
|
2011-08-26 00:08:53 +03:00
|
|
|
}
|
2015-02-14 21:42:47 +02:00
|
|
|
}
|
|
|
|
catch(...)
|
|
|
|
{
|
|
|
|
handleException();
|
2016-01-16 17:36:16 +02:00
|
|
|
}
|
2011-08-25 18:24:37 +03:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::battleFinished()
|
2017-03-14 01:40:39 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
stopAllBattleActions();
|
|
|
|
for(auto & side : gs->curB->sides)
|
|
|
|
if(battleCallbacks.count(side.color))
|
|
|
|
battleCallbacks[side.color]->setBattle(nullptr);
|
2013-06-22 17:47:20 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
|
|
|
|
battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr);
|
2013-06-22 17:47:20 +03:00
|
|
|
}
|
|
|
|
|
2017-11-16 13:15:43 +02:00
|
|
|
void CClient::startPlayerBattleAction(PlayerColor color)
|
|
|
|
{
|
|
|
|
stopPlayerBattleAction(color);
|
|
|
|
|
|
|
|
if(vstd::contains(battleints, color))
|
|
|
|
{
|
|
|
|
auto thread = std::make_shared<boost::thread>(std::bind(&CClient::waitForMoveAndSend, this, color));
|
|
|
|
playerActionThreads[color] = thread;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void CClient::stopPlayerBattleAction(PlayerColor color)
|
|
|
|
{
|
|
|
|
if(vstd::contains(playerActionThreads, color))
|
|
|
|
{
|
|
|
|
auto thread = playerActionThreads.at(color);
|
|
|
|
if(thread->joinable())
|
|
|
|
{
|
|
|
|
thread->interrupt();
|
|
|
|
thread->join();
|
|
|
|
}
|
|
|
|
playerActionThreads.erase(color);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void CClient::stopAllBattleActions()
|
|
|
|
{
|
|
|
|
while(!playerActionThreads.empty())
|
|
|
|
stopPlayerBattleAction(playerActionThreads.begin()->first);
|
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::waitForMoveAndSend(PlayerColor color)
|
2010-09-03 21:42:54 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
try
|
2017-06-04 14:59:11 +02:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
setThreadName("CClient::waitForMoveAndSend");
|
|
|
|
assert(vstd::contains(battleints, color));
|
|
|
|
BattleAction ba = battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false));
|
|
|
|
if(ba.actionType != EActionType::CANCEL)
|
|
|
|
{
|
|
|
|
logNetwork->trace("Send battle action to server: %s", ba.toString());
|
|
|
|
MakeAction temp_action(ba);
|
|
|
|
sendRequest(&temp_action, color);
|
|
|
|
}
|
2017-06-04 14:59:11 +02:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
catch(boost::thread_interrupted &)
|
2010-09-03 21:42:54 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
logNetwork->debug("Wait for move thread was interrupted and no action will be send. Was a battle ended by spell?");
|
2016-03-12 03:41:27 +02:00
|
|
|
}
|
|
|
|
catch(...)
|
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
handleException();
|
2016-03-12 03:41:27 +02:00
|
|
|
}
|
2010-09-03 21:42:54 +03:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
void CClient::invalidatePaths()
|
2010-09-03 21:42:54 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
// turn pathfinding info into invalid. It will be regenerated later
|
|
|
|
boost::unique_lock<boost::mutex> pathLock(pathInfo->pathMx);
|
|
|
|
pathInfo->hero = nullptr;
|
2010-09-03 21:42:54 +03:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
const CPathsInfo * CClient::getPathsInfo(const CGHeroInstance * h)
|
2010-09-03 21:42:54 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
assert(h);
|
|
|
|
boost::unique_lock<boost::mutex> pathLock(pathInfo->pathMx);
|
|
|
|
if(pathInfo->hero != h)
|
2012-11-16 00:29:22 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
gs->calculatePaths(h, *pathInfo.get());
|
2012-11-16 00:29:22 +03:00
|
|
|
}
|
2018-01-05 19:21:07 +02:00
|
|
|
return pathInfo.get();
|
2010-09-03 21:42:54 +03:00
|
|
|
}
|
2010-10-24 14:35:14 +03:00
|
|
|
|
2018-01-05 19:21:07 +02:00
|
|
|
PlayerColor CClient::getLocalPlayer() const
|
2010-10-24 14:35:14 +03:00
|
|
|
{
|
2018-01-05 19:21:07 +02:00
|
|
|
if(LOCPLINT)
|
|
|
|
return LOCPLINT->playerID;
|
|
|
|
return getCurrentPlayer();
|
2010-10-31 00:53:41 +03:00
|
|
|
}
|
2017-05-25 19:57:20 +02:00
|
|
|
|
|
|
|
#ifdef VCMI_ANDROID
|
|
|
|
extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jobject cls)
|
|
|
|
{
|
2017-08-10 18:39:27 +02:00
|
|
|
logNetwork->info("Received server ready signal");
|
2017-05-25 19:57:20 +02:00
|
|
|
androidTestServerReadyFlag.store(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
extern "C" JNIEXPORT bool JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jobject cls)
|
|
|
|
{
|
2017-08-10 18:39:27 +02:00
|
|
|
logGlobal->info("Received emergency save game request");
|
2017-05-25 19:57:20 +02:00
|
|
|
if(!LOCPLINT || !LOCPLINT->cb)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOCPLINT->cb->save("Saves/_Android_Autosave");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
#endif
|