1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-05-13 22:06:58 +02:00

Client: implement spectator mode via command-line options

If running with --spectate/-s CPlayerInterface will appear even without human players.
Following command-line options also available:
 --spectate-ignore-hero
 --spectate-hero-speed=N
 --spectate-battle-speed=N
 --spectate-skip-battle
 --spectate-skip-battle-result
Boolean options can also be changed in runtime via client console:
 set spectate-ignore-hero on / off
Spectator mode also:
 - Work with --onlyAI option when starting game or loading saves.
 - Allow to use any cheat codes.
 - Give recon on towns and heroes.
This commit is contained in:
Arseniy Shestakov 2017-06-03 08:25:10 +03:00
parent d95c74941b
commit 18161d3688
24 changed files with 196 additions and 54 deletions

View File

@ -1,3 +1,8 @@
0.99 -> 1.00
GENERAL:
* Spectator mode was implemented through command-line options
0.98 -> 0.99 0.98 -> 0.99
GENERAL: GENERAL:

View File

@ -123,7 +123,7 @@ void startTestMap(const std::string &mapname)
PlayerSettings &pset = si.playerInfos[PlayerColor(i)]; PlayerSettings &pset = si.playerInfos[PlayerColor(i)];
pset.color = PlayerColor(i); pset.color = PlayerColor(i);
pset.name = CGI->generaltexth->allTexts[468];//Computer pset.name = CGI->generaltexth->allTexts[468];//Computer
pset.playerID = i; pset.playerID = PlayerSettings::PLAYER_AI;
pset.compOnly = true; pset.compOnly = true;
pset.castle = 0; pset.castle = 0;
pset.hero = -1; pset.hero = -1;
@ -263,6 +263,12 @@ int main(int argc, char** argv)
("battle,b", po::value<std::string>(), "runs game in duel mode (battle-only") ("battle,b", po::value<std::string>(), "runs game in duel mode (battle-only")
("start", po::value<bfs::path>(), "starts game from saved StartInfo file") ("start", po::value<bfs::path>(), "starts game from saved StartInfo file")
("testmap", po::value<std::string>(), "") ("testmap", po::value<std::string>(), "")
("spectate,s", "enable spectator interface for AI-only games")
("spectate-ignore-hero", "wont follow heroes on adventure map")
("spectate-hero-speed", po::value<int>(), "hero movement speed on adventure map")
("spectate-battle-speed", po::value<int>(), "battle animation speed for spectator")
("spectate-skip-battle", "skip battles in spectator view")
("spectate-skip-battle-result", "skip battle result window")
("onlyAI", "runs without human player, all players will be default AI") ("onlyAI", "runs without human player, all players will be default AI")
("headless", "runs without GUI, implies --onlyAI") ("headless", "runs without GUI, implies --onlyAI")
("ai", po::value<std::vector<std::string>>(), "AI to be used for the player, can be specified several times for the consecutive players") ("ai", po::value<std::vector<std::string>>(), "AI to be used for the player, can be specified several times for the consecutive players")
@ -515,18 +521,34 @@ int main(int argc, char** argv)
if(vm.count("testmap")) if(vm.count("testmap"))
testmap = vm["testmap"].as<std::string>(); testmap = vm["testmap"].as<std::string>();
session["spectate"].Bool() = vm.count("spectate");
if(session["spectate"].Bool())
{
session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero");
session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle");
session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result");
if(vm.count("spectate-hero-speed"))
session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as<int>();
if(vm.count("spectate-battle-speed"))
session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as<int>();
}
if(!testmap.empty()) if(!testmap.empty())
{
startTestMap(testmap); startTestMap(testmap);
else if(!fileToStartFrom.empty() && bfs::exists(fileToStartFrom)) }
startGameFromFile(fileToStartFrom); //ommit pregame and start the game using settings from file
else else
{ {
if(!fileToStartFrom.empty()) if(!fileToStartFrom.empty() && bfs::exists(fileToStartFrom))
startGameFromFile(fileToStartFrom); //ommit pregame and start the game using settings from file
else
{ {
logGlobal->warnStream() << "Warning: cannot find given file to start from (" << fileToStartFrom if(!fileToStartFrom.empty())
<< "). Falling back to main menu."; {
logGlobal->warnStream() << "Warning: cannot find given file to start from (" << fileToStartFrom
<< "). Falling back to main menu.";
}
GH.curInt = CGPreGame::create(); //will set CGP pointer to itself
} }
GH.curInt = CGPreGame::create(); //will set CGP pointer to itself
} }
} }
else else

View File

@ -296,6 +296,8 @@ void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor play
void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y) void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y)
{ {
if(playerColor.isSpectator())
playerColor = PlayerColor(1);
std::vector<const IImage*> &box = piecesOfBox.at(playerColor.getNum()); std::vector<const IImage*> &box = piecesOfBox.at(playerColor.getNum());
// Note: this code assumes that the corner dimensions are all the same. // Note: this code assumes that the corner dimensions are all the same.

View File

@ -245,6 +245,9 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
if (LOCPLINT != this) if (LOCPLINT != this)
return; return;
if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool())
return;
const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero
int3 hp = details.start; int3 hp = details.start;
@ -321,7 +324,12 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
} }
ui32 speed; ui32 speed;
if (makingTurn) // our turn, our hero moves if(settings["session"]["spectate"].Bool())
{
if(!settings["session"]["spectate-hero-speed"].isNull())
speed = settings["session"]["spectate-hero-speed"].Integer();
}
else if (makingTurn) // our turn, our hero moves
speed = settings["adventure"]["heroSpeed"].Float(); speed = settings["adventure"]["heroSpeed"].Float();
else else
speed = settings["adventure"]["enemySpeed"].Float(); speed = settings["adventure"]["enemySpeed"].Float();
@ -334,7 +342,6 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details)
return; // no animation return; // no animation
} }
adventureInt->centerOn(hero); //actualizing screen pos adventureInt->centerOn(hero); //actualizing screen pos
adventureInt->minimap.redraw(); adventureInt->minimap.redraw();
adventureInt->heroList.redraw(); adventureInt->heroList.redraw();
@ -471,6 +478,14 @@ int3 CPlayerInterface::repairScreenPos(int3 pos)
pos.y = CGI->mh->sizes.y - adventureInt->terrain.tilesh + CGI->mh->frameH; pos.y = CGI->mh->sizes.y - adventureInt->terrain.tilesh + CGI->mh->frameH;
return pos; return pos;
} }
void CPlayerInterface::activateForSpectator()
{
adventureInt->state = CAdvMapInt::INGAME;
adventureInt->activate();
adventureInt->minimap.activate();
}
void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val)
{ {
EVENT_HANDLER_CALLED_BY_CLIENT; EVENT_HANDLER_CALLED_BY_CLIENT;
@ -1637,7 +1652,8 @@ void CPlayerInterface::update()
} }
//in some conditions we may receive calls before selection is initialized - we must ignore them //in some conditions we may receive calls before selection is initialized - we must ignore them
if (adventureInt && !adventureInt->selection && GH.topInt() == adventureInt) if(adventureInt && GH.topInt() == adventureInt
&& (!adventureInt->selection && !settings["session"]["spectate"].Bool()))
{ {
return; return;
} }
@ -2131,7 +2147,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
--howManyPeople; --howManyPeople;
if (howManyPeople == 0) //all human players eliminated if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated
{ {
if (adventureInt) if (adventureInt)
{ {
@ -2152,7 +2168,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
} }
else else
{ {
if (howManyPeople == 0) //all human players eliminated if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated
{ {
requestReturningToMainMenu(); requestReturningToMainMenu();
} }

View File

@ -236,6 +236,7 @@ public:
void updateInfo(const CGObjectInstance * specific); void updateInfo(const CGObjectInstance * specific);
void init(std::shared_ptr<CCallback> CB) override; void init(std::shared_ptr<CCallback> CB) override;
int3 repairScreenPos(int3 pos); //returns position closest to pos we can center screen on int3 repairScreenPos(int3 pos); //returns position closest to pos we can center screen on
void activateForSpectator(); // TODO: spectator probably need own player interface class
// show dialogs // show dialogs
void showInfoDialog(const std::string &text, CComponent * component); void showInfoDialog(const std::string &text, CComponent * component);

View File

@ -487,6 +487,10 @@ void CClient::newGame( CConnection *con, StartInfo *si )
} }
else else
{ {
if(settings["session"]["spectate"].Bool())
{
installNewPlayerInterface(std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true);
}
loadNeutralBattleAI(); loadNeutralBattleAI();
} }
@ -638,9 +642,6 @@ void CClient::serialize(BinaryDeserializer & h, const int version, const std::se
nInt->human = isHuman; nInt->human = isHuman;
nInt->playerID = pid; nInt->playerID = pid;
if(playerIDs.count(pid))
installNewPlayerInterface(nInt, pid);
nInt->loadGame(h, version); nInt->loadGame(h, version);
if(settings["session"]["onlyai"].Bool() && isHuman) if(settings["session"]["onlyai"].Bool() && isHuman)
{ {
@ -654,6 +655,20 @@ void CClient::serialize(BinaryDeserializer & h, const int version, const std::se
installNewPlayerInterface(nInt, pid); installNewPlayerInterface(nInt, pid);
GH.totalRedraw(); GH.totalRedraw();
} }
else
{
if(playerIDs.count(pid))
installNewPlayerInterface(nInt, pid);
}
}
if(settings["session"]["spectate"].Bool())
{
removeGUI();
auto p = std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR);
installNewPlayerInterface(p, PlayerColor::SPECTATOR, true);
GH.curInt = p.get();
LOCPLINT->activateForSpectator();
GH.totalRedraw();
} }
if(playerIDs.count(PlayerColor::NEUTRAL)) if(playerIDs.count(PlayerColor::NEUTRAL))
@ -759,15 +774,29 @@ void CClient::battleStarted(const BattleInfo * info)
def = std::dynamic_pointer_cast<CPlayerInterface>( playerint[rightSide.color] ); def = std::dynamic_pointer_cast<CPlayerInterface>( playerint[rightSide.color] );
} }
if(!settings["session"]["headless"].Bool() if(!settings["session"]["headless"].Bool())
&& (!!att || !!def || gs->scenarioOps->mode == StartInfo::DUEL))
{ {
boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim); if(!!att || !!def || gs->scenarioOps->mode == StartInfo::DUEL)
auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, {
Rect((screen->w - 800)/2, boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
(screen->h - 600)/2, 800, 600), att, def); 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); GH.pushInt(bi);
}
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);
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, spectratorInt);
GH.pushInt(bi);
}
} }
auto callBattleStart = [&](PlayerColor color, ui8 side){ auto callBattleStart = [&](PlayerColor color, ui8 side){
@ -778,6 +807,8 @@ void CClient::battleStarted(const BattleInfo * info)
callBattleStart(leftSide.color, 0); callBattleStart(leftSide.color, 0);
callBattleStart(rightSide.color, 1); callBattleStart(rightSide.color, 1);
callBattleStart(PlayerColor::UNFLAGGABLE, 1); callBattleStart(PlayerColor::UNFLAGGABLE, 1);
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
callBattleStart(PlayerColor::SPECTATOR, 1);
if(info->tacticDistance && vstd::contains(battleints,info->sides[info->tacticsSide].color)) if(info->tacticDistance && vstd::contains(battleints,info->sides[info->tacticsSide].color))
{ {
@ -790,6 +821,9 @@ void CClient::battleFinished()
for(auto & side : gs->curB->sides) for(auto & side : gs->curB->sides)
if(battleCallbacks.count(side.color)) if(battleCallbacks.count(side.color))
battleCallbacks[side.color]->setBattle(nullptr); battleCallbacks[side.color]->setBattle(nullptr);
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr);
} }
void CClient::loadNeutralBattleAI() void CClient::loadNeutralBattleAI()
@ -887,7 +921,7 @@ void CClient::campaignMapFinished( std::shared_ptr<CCampaignState> camp )
} }
} }
void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color) void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color, bool battlecb)
{ {
boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim); boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE); PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE);
@ -903,7 +937,7 @@ void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInte
battleCallbacks[colorUsed] = cb; battleCallbacks[colorUsed] = cb;
gameInterface->init(cb); gameInterface->init(cb);
installNewBattleInterface(gameInterface, color, false); installNewBattleInterface(gameInterface, color, battlecb);
} }
void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback /*= true*/) void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback /*= true*/)

View File

@ -148,7 +148,7 @@ public:
void newGame(CConnection *con, StartInfo *si); //con - connection to server void newGame(CConnection *con, StartInfo *si); //con - connection to server
void loadNeutralBattleAI(); void loadNeutralBattleAI();
void installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color); void installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, boost::optional<PlayerColor> color, bool battlecb = false);
void installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback = true); void installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, boost::optional<PlayerColor> color, bool needCallback = true);
std::string aiNameForPlayer(const PlayerSettings &ps, bool battleAI); //empty means no AI -> human std::string aiNameForPlayer(const PlayerSettings &ps, bool battleAI); //empty means no AI -> human
std::string aiNameForPlayer(bool battleAI); std::string aiNameForPlayer(bool battleAI);

View File

@ -96,6 +96,10 @@
#define BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(function,...) \ #define BATTLE_INTERFACE_CALL_IF_PRESENT_FOR_BOTH_SIDES(function,...) \
CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[0].color, function, __VA_ARGS__) \ CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[0].color, function, __VA_ARGS__) \
CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[1].color, function, __VA_ARGS__) \ CALL_ONLY_THAT_BATTLE_INTERFACE(GS(cl)->curB->sides[1].color, function, __VA_ARGS__) \
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) \
{ \
CALL_ONLY_THAT_BATTLE_INTERFACE(PlayerColor::SPECTATOR, function, __VA_ARGS__) \
} \
BATTLE_INTERFACE_CALL_RECEIVERS(function, __VA_ARGS__) BATTLE_INTERFACE_CALL_RECEIVERS(function, __VA_ARGS__)
/* /*
* NetPacksClient.cpp, part of VCMI engine * NetPacksClient.cpp, part of VCMI engine
@ -358,12 +362,12 @@ void TryMoveHero::applyFirstCl(CClient *cl)
//check if playerint will have the knowledge about movement - if not, directly update maphandler //check if playerint will have the knowledge about movement - if not, directly update maphandler
for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
{ {
if(i->first >= PlayerColor::PLAYER_LIMIT) auto ps = GS(cl)->getPlayer(i->first);
continue; if(ps && (GS(cl)->isVisible(start - int3(1, 0, 0), i->first) || GS(cl)->isVisible(end - int3(1, 0, 0), i->first)))
TeamState *t = GS(cl)->getPlayerTeam(i->first); {
if((t->fogOfWarMap[start.x-1][start.y][start.z] || t->fogOfWarMap[end.x-1][end.y][end.z]) if(ps->human)
&& GS(cl)->getPlayer(i->first)->human) humanKnows = true;
humanKnows = true; }
} }
if(!CGI->mh) if(!CGI->mh)
@ -399,9 +403,8 @@ void TryMoveHero::applyCl(CClient *cl)
//notify interfaces about move //notify interfaces about move
for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++)
{ {
if(i->first >= PlayerColor::PLAYER_LIMIT) continue; if(GS(cl)->isVisible(start - int3(1, 0, 0), i->first)
TeamState *t = GS(cl)->getPlayerTeam(i->first); || GS(cl)->isVisible(end - int3(1, 0, 0), i->first))
if(t->fogOfWarMap[start.x-1][start.y][start.z] || t->fogOfWarMap[end.x-1][end.y][end.z])
{ {
i->second->heroMoved(*this); i->second->heroMoved(*this);
} }
@ -592,6 +595,8 @@ void BattleStart::applyFirstCl(CClient *cl)
info->tile, info->sides[0].hero, info->sides[1].hero); info->tile, info->sides[0].hero, info->sides[1].hero);
CALL_ONLY_THAT_BATTLE_INTERFACE(info->sides[1].color, battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject, CALL_ONLY_THAT_BATTLE_INTERFACE(info->sides[1].color, battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject,
info->tile, info->sides[0].hero, info->sides[1].hero); info->tile, info->sides[0].hero, info->sides[1].hero);
CALL_ONLY_THAT_BATTLE_INTERFACE(PlayerColor::SPECTATOR, battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject,
info->tile, info->sides[0].hero, info->sides[1].hero);
BATTLE_INTERFACE_CALL_RECEIVERS(battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject, BATTLE_INTERFACE_CALL_RECEIVERS(battleStartBefore, info->sides[0].armyObject, info->sides[1].armyObject,
info->tile, info->sides[0].hero, info->sides[1].hero); info->tile, info->sides[0].hero, info->sides[1].hero);
} }
@ -711,7 +716,7 @@ void BattleResultsApplied::applyCl(CClient *cl)
{ {
INTERFACE_CALL_IF_PRESENT(player1, battleResultsApplied); INTERFACE_CALL_IF_PRESENT(player1, battleResultsApplied);
INTERFACE_CALL_IF_PRESENT(player2, battleResultsApplied); INTERFACE_CALL_IF_PRESENT(player2, battleResultsApplied);
INTERFACE_CALL_IF_PRESENT(PlayerColor::UNFLAGGABLE, battleResultsApplied); INTERFACE_CALL_IF_PRESENT(PlayerColor::SPECTATOR, battleResultsApplied);
if(GS(cl)->initialOpts->mode == StartInfo::DUEL) if(GS(cl)->initialOpts->mode == StartInfo::DUEL)
{ {
handleQuit(); handleQuit();
@ -812,7 +817,10 @@ void PlayerMessage::applyCl(CClient *cl)
logNetwork->debugStream() << "Player "<< player <<" sends a message: " << text; logNetwork->debugStream() << "Player "<< player <<" sends a message: " << text;
std::ostringstream str; std::ostringstream str;
str << cl->getPlayer(player)->nodeName() <<": " << text; if(player.isSpectator())
str << "Spectator: " << text;
else
str << cl->getPlayer(player)->nodeName() <<": " << text;
if(LOCPLINT) if(LOCPLINT)
LOCPLINT->cingconsole->print(str.str()); LOCPLINT->cingconsole->print(str.str());
} }

View File

@ -95,7 +95,7 @@ void CBattleInterface::addNewAnim(CBattleAnimation *anim)
CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2,
const CGHeroInstance *hero1, const CGHeroInstance *hero2, const CGHeroInstance *hero1, const CGHeroInstance *hero2,
const SDL_Rect & myRect, const SDL_Rect & myRect,
std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen) std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt)
: background(nullptr), queue(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0), : background(nullptr), queue(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0),
activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1), activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1),
currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr), currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr),
@ -105,12 +105,15 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
{ {
OBJ_CONSTRUCTION; OBJ_CONSTRUCTION;
if (!curInt) if(spectatorInt)
curInt = spectatorInt;
else if(!curInt)
{ {
//May happen when we are defending during network MP game -> attacker interface is just not present //May happen when we are defending during network MP game -> attacker interface is just not present
curInt = defenderInt; curInt = defenderInt;
} }
animsAreDisplayed.setn(false); animsAreDisplayed.setn(false);
pos = myRect; pos = myRect;
strongInterest = true; strongInterest = true;
@ -377,6 +380,8 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
currentAction = INVALID; currentAction = INVALID;
selectedAction = INVALID; selectedAction = INVALID;
addUsedEvents(RCLICK | MOVE | KEYBOARD); addUsedEvents(RCLICK | MOVE | KEYBOARD);
blockUI(settings["session"]["spectate"].Bool());
} }
CBattleInterface::~CBattleInterface() CBattleInterface::~CBattleInterface()
@ -1246,6 +1251,11 @@ void CBattleInterface::battleFinished(const BattleResult& br)
void CBattleInterface::displayBattleFinished() void CBattleInterface::displayBattleFinished()
{ {
CCS->curh->changeGraphic(ECursor::ADVENTURE,0); CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool())
{
GH.popIntTotally(this);
return;
}
SDL_Rect temp_rect = genRect(561, 470, (screen->w - 800)/2 + 165, (screen->h - 600)/2 + 19); SDL_Rect temp_rect = genRect(561, 470, (screen->w - 800)/2 + 165, (screen->h - 600)/2 + 19);
resWindow = new CBattleResultWindow(*bresult, temp_rect, *this->curInt); resWindow = new CBattleResultWindow(*bresult, temp_rect, *this->curInt);
@ -1531,6 +1541,9 @@ void CBattleInterface::setAnimSpeed(int set)
int CBattleInterface::getAnimSpeed() const int CBattleInterface::getAnimSpeed() const
{ {
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-battle-speed"].isNull())
return vstd::round(settings["session"]["spectate-battle-speed"].Float() *100);
return vstd::round(settings["battle"]["animationSpeed"].Float() *100); return vstd::round(settings["battle"]["animationSpeed"].Float() *100);
} }

View File

@ -269,7 +269,7 @@ public:
ui32 animIDhelper; //for giving IDs for animations ui32 animIDhelper; //for giving IDs for animations
static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims
CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen); //c-tor CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr); //c-tor
virtual ~CBattleInterface(); //d-tor virtual ~CBattleInterface(); //d-tor
//std::vector<TimeInterested*> timeinterested; //animation handling //std::vector<TimeInterested*> timeinterested; //animation handling

View File

@ -211,8 +211,7 @@ void CBattleHero::clickRight(tribool down, bool previousState)
windowPosition.y = myOwner->pos.y + 135; windowPosition.y = myOwner->pos.y + 135;
InfoAboutHero targetHero; InfoAboutHero targetHero;
if(down && (myOwner->myTurn || settings["session"]["spectate"].Bool()))
if (down && myOwner->myTurn)
{ {
auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance; auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance;
targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE); targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);

View File

@ -361,7 +361,8 @@ void CGuiHandler::simpleRedraw()
//update only top interface and draw background //update only top interface and draw background
if(objsToBlit.size() > 1) if(objsToBlit.size() > 1)
blitAt(screen2,0,0,screen); //blit background blitAt(screen2,0,0,screen); //blit background
objsToBlit.back()->show(screen); //blit active interface/window if(!objsToBlit.empty())
objsToBlit.back()->show(screen); //blit active interface/window
} }
void CGuiHandler::handleMoveInterested(const SDL_MouseMotionEvent & motion) void CGuiHandler::handleMoveInterested(const SDL_MouseMotionEvent & motion)

View File

@ -54,7 +54,7 @@ struct NeighborTilesInfo
if ( dx + pos.x < 0 || dx + pos.x >= sizes.x if ( dx + pos.x < 0 || dx + pos.x >= sizes.x
|| dy + pos.y < 0 || dy + pos.y >= sizes.y) || dy + pos.y < 0 || dy + pos.y >= sizes.y)
return false; return false;
return visibilityMap[dx+pos.x][dy+pos.y][pos.z]; return settings["session"]["spectate"].Bool() ? true : visibilityMap[dx+pos.x][dy+pos.y][pos.z];
}; };
d7 = getTile(-1, -1); //789 d7 = getTile(-1, -1); //789
d8 = getTile( 0, -1); //456 d8 = getTile( 0, -1); //456
@ -563,7 +563,7 @@ void CMapHandler::CMapWorldViewBlitter::drawTileOverlay(SDL_Surface * targetSurf
const CGObjectInstance * obj = object.obj; const CGObjectInstance * obj = object.obj;
const bool sameLevel = obj->pos.z == pos.z; const bool sameLevel = obj->pos.z == pos.z;
const bool isVisible = (*info->visibilityMap)[pos.x][pos.y][pos.z]; const bool isVisible = settings["session"]["spectate"].Bool() ? true : (*info->visibilityMap)[pos.x][pos.y][pos.z];
const bool isVisitable = obj->visitableAt(pos.x, pos.y); const bool isVisitable = obj->visitableAt(pos.x, pos.y);
if(sameLevel && isVisible && isVisitable) if(sameLevel && isVisible && isVisitable)
@ -895,7 +895,7 @@ void CMapHandler::CMapBlitter::blit(SDL_Surface * targetSurf, const MapDrawingIn
{ {
const TerrainTile2 & tile = parent->ttiles[pos.x][pos.y][pos.z]; const TerrainTile2 & tile = parent->ttiles[pos.x][pos.y][pos.z];
if (!(*info->visibilityMap)[pos.x][pos.y][topTile.z] && !info->showAllTerrain) if(!settings["session"]["spectate"].Bool() && !(*info->visibilityMap)[pos.x][pos.y][topTile.z] && !info->showAllTerrain)
drawFow(targetSurf); drawFow(targetSurf);
// overlay needs to be drawn over fow, because of artifacts-aura-like spells // overlay needs to be drawn over fow, because of artifacts-aura-like spells
@ -1099,6 +1099,9 @@ bool CMapHandler::CMapBlitter::canDrawObject(const CGObjectInstance * obj) const
bool CMapHandler::CMapBlitter::canDrawCurrentTile() const bool CMapHandler::CMapBlitter::canDrawCurrentTile() const
{ {
if(settings["session"]["spectate"].Bool())
return true;
const NeighborTilesInfo neighbors(pos, parent->sizes, *info->visibilityMap); const NeighborTilesInfo neighbors(pos, parent->sizes, *info->visibilityMap);
return !neighbors.areAllHidden(); return !neighbors.areAllHidden();
} }

View File

@ -1021,7 +1021,12 @@ void CAdvMapInt::show(SDL_Surface * to)
#endif #endif
for(int i = 0; i < 4; i++) for(int i = 0; i < 4; i++)
gems[i]->setFrame(LOCPLINT->playerID.getNum()); {
if(settings["session"]["spectate"].Bool())
gems[i]->setFrame(PlayerColor(1).getNum());
else
gems[i]->setFrame(LOCPLINT->playerID.getNum());
}
if(updateScreen) if(updateScreen)
{ {
int3 betterPos = LOCPLINT->repairScreenPos(position); int3 betterPos = LOCPLINT->repairScreenPos(position);
@ -1481,7 +1486,8 @@ void CAdvMapInt::setPlayer(PlayerColor Player)
void CAdvMapInt::startTurn() void CAdvMapInt::startTurn()
{ {
state = INGAME; state = INGAME;
if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID) if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID
|| settings["session"]["spectate"].Bool())
{ {
adjustActiveness(false); adjustActiveness(false);
minimap.setAIRadar(false); minimap.setAIRadar(false);
@ -1490,6 +1496,9 @@ void CAdvMapInt::startTurn()
void CAdvMapInt::endingTurn() void CAdvMapInt::endingTurn()
{ {
if(settings["session"]["spectate"].Bool())
return;
if(LOCPLINT->cingconsole->active) if(LOCPLINT->cingconsole->active)
LOCPLINT->cingconsole->deactivate(); LOCPLINT->cingconsole->deactivate();
LOCPLINT->makingTurn = false; LOCPLINT->makingTurn = false;
@ -1817,6 +1826,9 @@ const IShipyard * CAdvMapInt::ourInaccessibleShipyard(const CGObjectInstance *ob
void CAdvMapInt::aiTurnStarted() void CAdvMapInt::aiTurnStarted()
{ {
if(settings["session"]["spectate"].Bool())
return;
adjustActiveness(true); adjustActiveness(true);
CCS->musich->playMusicFromSet("enemy-turn", true); CCS->musich->playMusicFromSet("enemy-turn", true);
adventureInt->minimap.setAIRadar(true); adventureInt->minimap.setAIRadar(true);

View File

@ -225,9 +225,13 @@ void CWindowObject::setShadow(bool on)
void CWindowObject::showAll(SDL_Surface *to) void CWindowObject::showAll(SDL_Surface *to)
{ {
auto color = LOCPLINT ? LOCPLINT->playerID : PlayerColor(1);
if(settings["session"]["spectate"].Bool())
color = PlayerColor(1); // TODO: Spectator shouldn't need special code for UI colors
CIntObject::showAll(to); CIntObject::showAll(to);
if ((options & BORDERED) && (pos.h != to->h || pos.w != to->w)) if ((options & BORDERED) && (pos.h != to->h || pos.w != to->w))
CMessage::drawBorder(LOCPLINT ? LOCPLINT->playerID : PlayerColor(1), to, pos.w+28, pos.h+29, pos.x-14, pos.y-15); CMessage::drawBorder(color, to, pos.w+28, pos.h+29, pos.x-14, pos.y-15);
} }
void CWindowObject::close() void CWindowObject::close()

View File

@ -335,6 +335,8 @@ void CRClickPopup::close()
void CRClickPopup::createAndPush(const std::string &txt, const CInfoWindow::TCompsInfo &comps) void CRClickPopup::createAndPush(const std::string &txt, const CInfoWindow::TCompsInfo &comps)
{ {
PlayerColor player = LOCPLINT ? LOCPLINT->playerID : PlayerColor(1); //if no player, then use blue PlayerColor player = LOCPLINT ? LOCPLINT->playerID : PlayerColor(1); //if no player, then use blue
if(settings["session"]["spectate"].Bool())//TODO: there must be better way to implement this
player = PlayerColor(1);
CSimpleWindow * temp = new CInfoWindow(txt, player, comps); CSimpleWindow * temp = new CInfoWindow(txt, player, comps);
temp->center(Point(GH.current->motion)); //center on mouse temp->center(Point(GH.current->motion)); //center on mouse

View File

@ -237,7 +237,7 @@ const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const
BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const
{ {
RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID);
if(!player) if(!player || player.get().isSpectator())
return BattlePerspective::ALL_KNOWING; return BattlePerspective::ALL_KNOWING;
if(*player == getBattle()->sides[0].color) if(*player == getBattle()->sides[0].color)
return BattlePerspective::LEFT_SIDE; return BattlePerspective::LEFT_SIDE;

View File

@ -595,7 +595,7 @@ const CMapHeader * CGameInfoCallback::getMapHeader() const
bool CGameInfoCallback::hasAccess(boost::optional<PlayerColor> playerId) const bool CGameInfoCallback::hasAccess(boost::optional<PlayerColor> playerId) const
{ {
return !player || gs->getPlayerRelations( *playerId, *player ) != PlayerRelations::ENEMIES; return !player || player.get().isSpectator() || gs->getPlayerRelations( *playerId, *player ) != PlayerRelations::ENEMIES;
} }
EPlayerStatus::EStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const EPlayerStatus::EStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const

View File

@ -2208,6 +2208,9 @@ bool CGameState::isVisible(int3 pos, PlayerColor player)
{ {
if(player == PlayerColor::NEUTRAL) if(player == PlayerColor::NEUTRAL)
return false; return false;
if(player.isSpectator())
return true;
return getPlayerTeam(player)->fogOfWarMap[pos.x][pos.y][pos.z]; return getPlayerTeam(player)->fogOfWarMap[pos.x][pos.y][pos.z];
} }

View File

@ -29,6 +29,7 @@ const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER = SlotID(-3);
const SlotID SlotID::WAR_MACHINES_SLOT = SlotID(-4); const SlotID SlotID::WAR_MACHINES_SLOT = SlotID(-4);
const SlotID SlotID::ARROW_TOWERS_SLOT = SlotID(-5); const SlotID SlotID::ARROW_TOWERS_SLOT = SlotID(-5);
const PlayerColor PlayerColor::SPECTATOR = PlayerColor(252);
const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(253); const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(253);
const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(254); const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(254);
const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255); const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255);
@ -72,6 +73,11 @@ bool PlayerColor::isValidPlayer() const
return num < PLAYER_LIMIT_I; return num < PLAYER_LIMIT_I;
} }
bool PlayerColor::isSpectator() const
{
return num == 252;
}
std::string PlayerColor::getStr(bool L10n) const std::string PlayerColor::getStr(bool L10n) const
{ {
std::string ret = "unnamed"; std::string ret = "unnamed";

View File

@ -257,12 +257,14 @@ class PlayerColor : public BaseForID<PlayerColor, ui8>
PLAYER_LIMIT_I = 8 PLAYER_LIMIT_I = 8
}; };
DLL_LINKAGE static const PlayerColor SPECTATOR; //252
DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253 DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253
DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks)
DLL_LINKAGE static const PlayerColor NEUTRAL; //255 DLL_LINKAGE static const PlayerColor NEUTRAL; //255
DLL_LINKAGE static const PlayerColor PLAYER_LIMIT; //player limit per map DLL_LINKAGE static const PlayerColor PLAYER_LIMIT; //player limit per map
DLL_LINKAGE bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) DLL_LINKAGE bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral)
DLL_LINKAGE bool isSpectator() const;
DLL_LINKAGE std::string getStr(bool L10n = false) const; DLL_LINKAGE std::string getStr(bool L10n = false) const;
DLL_LINKAGE std::string getStrCap(bool L10n = false) const; DLL_LINKAGE std::string getStrCap(bool L10n = false) const;

View File

@ -1842,6 +1842,9 @@ DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState *gs)
DLL_LINKAGE void PlayerCheated::applyGs(CGameState *gs) DLL_LINKAGE void PlayerCheated::applyGs(CGameState *gs)
{ {
if(!player.isValidPlayer())
return;
gs->getPlayer(player)->enteredLosingCheatCode = losingCheatCode; gs->getPlayer(player)->enteredLosingCheatCode = losingCheatCode;
gs->getPlayer(player)->enteredWinningCheatCode = winningCheatCode; gs->getPlayer(player)->enteredWinningCheatCode = winningCheatCode;
} }

View File

@ -1878,7 +1878,8 @@ void CGameHandler::run(bool resume)
sbuffer << color << " "; sbuffer << color << " ";
{ {
boost::unique_lock<boost::recursive_mutex> lock(gsm); boost::unique_lock<boost::recursive_mutex> lock(gsm);
connections[color] = cc; if(!color.isSpectator()) // there can be more than one spectator
connections[color] = cc;
} }
} }
logGlobal->info(sbuffer.str()); logGlobal->info(sbuffer.str());
@ -4430,7 +4431,9 @@ void CGameHandler::playerMessage(PlayerColor player, const std::string &message,
{ {
SystemMessage temp_message(VLC->generaltexth->allTexts.at(260)); SystemMessage temp_message(VLC->generaltexth->allTexts.at(260));
sendAndApply(&temp_message); sendAndApply(&temp_message);
checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature
if(!player.isSpectator())
checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature
} }
} }

View File

@ -283,8 +283,11 @@ bool CastAdvSpell::applyGh( CGameHandler *gh )
bool PlayerMessage::applyGh( CGameHandler *gh ) bool PlayerMessage::applyGh( CGameHandler *gh )
{ {
ERROR_IF_NOT(player); if(!player.isSpectator()) // TODO: clearly not a great way to verify permissions
if(gh->getPlayerAt(c) != player) ERROR_AND_RETURN; {
ERROR_IF_NOT(player);
if(gh->getPlayerAt(c) != player) ERROR_AND_RETURN;
}
gh->playerMessage(player,text, currObj); gh->playerMessage(player,text, currObj);
return true; return true;
} }