diff --git a/ChangeLog b/ChangeLog index 53890c598..afb527b7b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +0.99 -> 1.00 + +GENERAL: +* Spectator mode was implemented through command-line options + 0.98 -> 0.99 GENERAL: diff --git a/client/CMT.cpp b/client/CMT.cpp index 15f1b434a..14498e873 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -123,7 +123,7 @@ void startTestMap(const std::string &mapname) PlayerSettings &pset = si.playerInfos[PlayerColor(i)]; pset.color = PlayerColor(i); pset.name = CGI->generaltexth->allTexts[468];//Computer - pset.playerID = i; + pset.playerID = PlayerSettings::PLAYER_AI; pset.compOnly = true; pset.castle = 0; pset.hero = -1; @@ -263,6 +263,12 @@ int main(int argc, char** argv) ("battle,b", po::value(), "runs game in duel mode (battle-only") ("start", po::value(), "starts game from saved StartInfo file") ("testmap", po::value(), "") + ("spectate,s", "enable spectator interface for AI-only games") + ("spectate-ignore-hero", "wont follow heroes on adventure map") + ("spectate-hero-speed", po::value(), "hero movement speed on adventure map") + ("spectate-battle-speed", po::value(), "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") ("headless", "runs without GUI, implies --onlyAI") ("ai", po::value>(), "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")) testmap = vm["testmap"].as(); + 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(); + if(vm.count("spectate-battle-speed")) + session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as(); + } if(!testmap.empty()) + { startTestMap(testmap); - else if(!fileToStartFrom.empty() && bfs::exists(fileToStartFrom)) - startGameFromFile(fileToStartFrom); //ommit pregame and start the game using settings from file + } 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 - << "). Falling back to main menu."; + if(!fileToStartFrom.empty()) + { + 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 diff --git a/client/CMessage.cpp b/client/CMessage.cpp index 0106f8637..eb29a32fa 100644 --- a/client/CMessage.cpp +++ b/client/CMessage.cpp @@ -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) { + if(playerColor.isSpectator()) + playerColor = PlayerColor(1); std::vector &box = piecesOfBox.at(playerColor.getNum()); // Note: this code assumes that the corner dimensions are all the same. diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 3ebe3eda5..8970b8963 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -245,6 +245,9 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details) if (LOCPLINT != this) return; + if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool()) + return; + const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero int3 hp = details.start; @@ -321,7 +324,12 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details) } 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(); else speed = settings["adventure"]["enemySpeed"].Float(); @@ -334,7 +342,6 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details) return; // no animation } - adventureInt->centerOn(hero); //actualizing screen pos adventureInt->minimap.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; return pos; } + +void CPlayerInterface::activateForSpectator() +{ + adventureInt->state = CAdvMapInt::INGAME; + adventureInt->activate(); + adventureInt->minimap.activate(); +} + void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) { 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 - if (adventureInt && !adventureInt->selection && GH.topInt() == adventureInt) + if(adventureInt && GH.topInt() == adventureInt + && (!adventureInt->selection && !settings["session"]["spectate"].Bool())) { return; } @@ -2131,7 +2147,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul --howManyPeople; - if (howManyPeople == 0) //all human players eliminated + if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated { if (adventureInt) { @@ -2152,7 +2168,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul } else { - if (howManyPeople == 0) //all human players eliminated + if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated { requestReturningToMainMenu(); } diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 08d95c3dd..fc5e54d8d 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -236,6 +236,7 @@ public: void updateInfo(const CGObjectInstance * specific); void init(std::shared_ptr CB) override; 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 void showInfoDialog(const std::string &text, CComponent * component); diff --git a/client/Client.cpp b/client/Client.cpp index bf9d12878..f2a1c45e5 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -487,6 +487,10 @@ void CClient::newGame( CConnection *con, StartInfo *si ) } else { + if(settings["session"]["spectate"].Bool()) + { + installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); + } loadNeutralBattleAI(); } @@ -638,9 +642,6 @@ void CClient::serialize(BinaryDeserializer & h, const int version, const std::se nInt->human = isHuman; nInt->playerID = pid; - if(playerIDs.count(pid)) - installNewPlayerInterface(nInt, pid); - nInt->loadGame(h, version); if(settings["session"]["onlyai"].Bool() && isHuman) { @@ -654,6 +655,20 @@ void CClient::serialize(BinaryDeserializer & h, const int version, const std::se installNewPlayerInterface(nInt, pid); GH.totalRedraw(); } + else + { + if(playerIDs.count(pid)) + installNewPlayerInterface(nInt, pid); + } + } + if(settings["session"]["spectate"].Bool()) + { + removeGUI(); + auto p = std::make_shared(PlayerColor::SPECTATOR); + installNewPlayerInterface(p, PlayerColor::SPECTATOR, true); + GH.curInt = p.get(); + LOCPLINT->activateForSpectator(); + GH.totalRedraw(); } if(playerIDs.count(PlayerColor::NEUTRAL)) @@ -759,15 +774,29 @@ void CClient::battleStarted(const BattleInfo * info) def = std::dynamic_pointer_cast( playerint[rightSide.color] ); } - if(!settings["session"]["headless"].Bool() - && (!!att || !!def || gs->scenarioOps->mode == StartInfo::DUEL)) + if(!settings["session"]["headless"].Bool()) { - boost::unique_lock 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); + if(!!att || !!def || gs->scenarioOps->mode == StartInfo::DUEL) + { + boost::unique_lock 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); - 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(playerint[PlayerColor::SPECTATOR]); + spectratorInt->cb->setBattle(info); + boost::unique_lock 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){ @@ -778,6 +807,8 @@ void CClient::battleStarted(const BattleInfo * info) callBattleStart(leftSide.color, 0); callBattleStart(rightSide.color, 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)) { @@ -790,6 +821,9 @@ void CClient::battleFinished() for(auto & side : gs->curB->sides) if(battleCallbacks.count(side.color)) battleCallbacks[side.color]->setBattle(nullptr); + + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr); } void CClient::loadNeutralBattleAI() @@ -887,7 +921,7 @@ void CClient::campaignMapFinished( std::shared_ptr camp ) } } -void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, boost::optional color) +void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, boost::optional color, bool battlecb) { boost::unique_lock un(*CPlayerInterface::pim); PlayerColor colorUsed = color.get_value_or(PlayerColor::UNFLAGGABLE); @@ -903,7 +937,7 @@ void CClient::installNewPlayerInterface(std::shared_ptr gameInte battleCallbacks[colorUsed] = cb; gameInterface->init(cb); - installNewBattleInterface(gameInterface, color, false); + installNewBattleInterface(gameInterface, color, battlecb); } void CClient::installNewBattleInterface(std::shared_ptr battleInterface, boost::optional color, bool needCallback /*= true*/) diff --git a/client/Client.h b/client/Client.h index 27ebec442..3431e6f8d 100644 --- a/client/Client.h +++ b/client/Client.h @@ -148,7 +148,7 @@ public: void newGame(CConnection *con, StartInfo *si); //con - connection to server void loadNeutralBattleAI(); - void installNewPlayerInterface(std::shared_ptr gameInterface, boost::optional color); + void installNewPlayerInterface(std::shared_ptr gameInterface, boost::optional color, bool battlecb = false); void installNewBattleInterface(std::shared_ptr battleInterface, boost::optional color, bool needCallback = true); std::string aiNameForPlayer(const PlayerSettings &ps, bool battleAI); //empty means no AI -> human std::string aiNameForPlayer(bool battleAI); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 1a9949389..4e513322a 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -96,6 +96,10 @@ #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[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__) /* * 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 for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) { - if(i->first >= PlayerColor::PLAYER_LIMIT) - continue; - 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]) - && GS(cl)->getPlayer(i->first)->human) - humanKnows = true; + auto ps = GS(cl)->getPlayer(i->first); + if(ps && (GS(cl)->isVisible(start - int3(1, 0, 0), i->first) || GS(cl)->isVisible(end - int3(1, 0, 0), i->first))) + { + if(ps->human) + humanKnows = true; + } } if(!CGI->mh) @@ -399,9 +403,8 @@ void TryMoveHero::applyCl(CClient *cl) //notify interfaces about move for(auto i=cl->playerint.begin(); i!=cl->playerint.end(); i++) { - if(i->first >= PlayerColor::PLAYER_LIMIT) continue; - 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(GS(cl)->isVisible(start - int3(1, 0, 0), i->first) + || GS(cl)->isVisible(end - int3(1, 0, 0), i->first)) { i->second->heroMoved(*this); } @@ -592,6 +595,8 @@ void BattleStart::applyFirstCl(CClient *cl) 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, 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, 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(player2, battleResultsApplied); - INTERFACE_CALL_IF_PRESENT(PlayerColor::UNFLAGGABLE, battleResultsApplied); + INTERFACE_CALL_IF_PRESENT(PlayerColor::SPECTATOR, battleResultsApplied); if(GS(cl)->initialOpts->mode == StartInfo::DUEL) { handleQuit(); @@ -812,7 +817,10 @@ void PlayerMessage::applyCl(CClient *cl) logNetwork->debugStream() << "Player "<< player <<" sends a message: " << text; std::ostringstream str; - str << cl->getPlayer(player)->nodeName() <<": " << text; + if(player.isSpectator()) + str << "Spectator: " << text; + else + str << cl->getPlayer(player)->nodeName() <<": " << text; if(LOCPLINT) LOCPLINT->cingconsole->print(str.str()); } diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index 34696b825..1ab76bb8e 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -95,7 +95,7 @@ void CBattleInterface::addNewAnim(CBattleAnimation *anim) CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, - std::shared_ptr att, std::shared_ptr defen) + std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt) : background(nullptr), queue(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0), activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1), 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; - 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 curInt = defenderInt; } + animsAreDisplayed.setn(false); pos = myRect; strongInterest = true; @@ -377,6 +380,8 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet currentAction = INVALID; selectedAction = INVALID; addUsedEvents(RCLICK | MOVE | KEYBOARD); + + blockUI(settings["session"]["spectate"].Bool()); } CBattleInterface::~CBattleInterface() @@ -1246,6 +1251,11 @@ void CBattleInterface::battleFinished(const BattleResult& br) void CBattleInterface::displayBattleFinished() { 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); resWindow = new CBattleResultWindow(*bresult, temp_rect, *this->curInt); @@ -1531,6 +1541,9 @@ void CBattleInterface::setAnimSpeed(int set) 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); } diff --git a/client/battle/CBattleInterface.h b/client/battle/CBattleInterface.h index d61e52901..cfe6b2cb9 100644 --- a/client/battle/CBattleInterface.h +++ b/client/battle/CBattleInterface.h @@ -269,7 +269,7 @@ public: ui32 animIDhelper; //for giving IDs for animations static CondSh 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 att, std::shared_ptr defen); //c-tor + CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); //c-tor virtual ~CBattleInterface(); //d-tor //std::vector timeinterested; //animation handling diff --git a/client/battle/CBattleInterfaceClasses.cpp b/client/battle/CBattleInterfaceClasses.cpp index 1aa6f63a8..1c474380b 100644 --- a/client/battle/CBattleInterfaceClasses.cpp +++ b/client/battle/CBattleInterfaceClasses.cpp @@ -211,8 +211,7 @@ void CBattleHero::clickRight(tribool down, bool previousState) windowPosition.y = myOwner->pos.y + 135; InfoAboutHero targetHero; - - if (down && myOwner->myTurn) + if(down && (myOwner->myTurn || settings["session"]["spectate"].Bool())) { auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance; targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE); diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index aaa00637f..f26d7ac9a 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -361,7 +361,8 @@ void CGuiHandler::simpleRedraw() //update only top interface and draw background if(objsToBlit.size() > 1) 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) diff --git a/client/mapHandler.cpp b/client/mapHandler.cpp index 7efac572a..93b97cc29 100644 --- a/client/mapHandler.cpp +++ b/client/mapHandler.cpp @@ -54,7 +54,7 @@ struct NeighborTilesInfo if ( dx + pos.x < 0 || dx + pos.x >= sizes.x || dy + pos.y < 0 || dy + pos.y >= sizes.y) 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 d8 = getTile( 0, -1); //456 @@ -563,7 +563,7 @@ void CMapHandler::CMapWorldViewBlitter::drawTileOverlay(SDL_Surface * targetSurf const CGObjectInstance * obj = object.obj; 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); 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]; - 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); // 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 { + if(settings["session"]["spectate"].Bool()) + return true; + const NeighborTilesInfo neighbors(pos, parent->sizes, *info->visibilityMap); return !neighbors.areAllHidden(); } diff --git a/client/windows/CAdvmapInterface.cpp b/client/windows/CAdvmapInterface.cpp index 522068eb6..e82e34a9e 100644 --- a/client/windows/CAdvmapInterface.cpp +++ b/client/windows/CAdvmapInterface.cpp @@ -1021,7 +1021,12 @@ void CAdvMapInt::show(SDL_Surface * to) #endif 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) { int3 betterPos = LOCPLINT->repairScreenPos(position); @@ -1481,7 +1486,8 @@ void CAdvMapInt::setPlayer(PlayerColor Player) void CAdvMapInt::startTurn() { state = INGAME; - if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID) + if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID + || settings["session"]["spectate"].Bool()) { adjustActiveness(false); minimap.setAIRadar(false); @@ -1490,6 +1496,9 @@ void CAdvMapInt::startTurn() void CAdvMapInt::endingTurn() { + if(settings["session"]["spectate"].Bool()) + return; + if(LOCPLINT->cingconsole->active) LOCPLINT->cingconsole->deactivate(); LOCPLINT->makingTurn = false; @@ -1817,6 +1826,9 @@ const IShipyard * CAdvMapInt::ourInaccessibleShipyard(const CGObjectInstance *ob void CAdvMapInt::aiTurnStarted() { + if(settings["session"]["spectate"].Bool()) + return; + adjustActiveness(true); CCS->musich->playMusicFromSet("enemy-turn", true); adventureInt->minimap.setAIRadar(true); diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index e31e27c0e..05c2489d4 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -225,9 +225,13 @@ void CWindowObject::setShadow(bool on) 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); 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() diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index dcc6a3137..108440df8 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -335,6 +335,8 @@ void CRClickPopup::close() void CRClickPopup::createAndPush(const std::string &txt, const CInfoWindow::TCompsInfo &comps) { 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); temp->center(Point(GH.current->motion)); //center on mouse diff --git a/lib/CBattleCallback.cpp b/lib/CBattleCallback.cpp index 0cbeb40bb..fc5021e23 100644 --- a/lib/CBattleCallback.cpp +++ b/lib/CBattleCallback.cpp @@ -237,7 +237,7 @@ const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const { RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); - if(!player) + if(!player || player.get().isSpectator()) return BattlePerspective::ALL_KNOWING; if(*player == getBattle()->sides[0].color) return BattlePerspective::LEFT_SIDE; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index bb334d85b..28fd0b988 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -595,7 +595,7 @@ const CMapHeader * CGameInfoCallback::getMapHeader() const bool CGameInfoCallback::hasAccess(boost::optional 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 diff --git a/lib/CGameState.cpp b/lib/CGameState.cpp index c3f0e53b4..2a9dfcf89 100644 --- a/lib/CGameState.cpp +++ b/lib/CGameState.cpp @@ -2208,6 +2208,9 @@ bool CGameState::isVisible(int3 pos, PlayerColor player) { if(player == PlayerColor::NEUTRAL) return false; + if(player.isSpectator()) + return true; + return getPlayerTeam(player)->fogOfWarMap[pos.x][pos.y][pos.z]; } diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 6cbe2c43f..f6b73581a 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -29,6 +29,7 @@ const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER = SlotID(-3); const SlotID SlotID::WAR_MACHINES_SLOT = SlotID(-4); const SlotID SlotID::ARROW_TOWERS_SLOT = SlotID(-5); +const PlayerColor PlayerColor::SPECTATOR = PlayerColor(252); const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(253); const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(254); const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255); @@ -72,6 +73,11 @@ bool PlayerColor::isValidPlayer() const return num < PLAYER_LIMIT_I; } +bool PlayerColor::isSpectator() const +{ + return num == 252; +} + std::string PlayerColor::getStr(bool L10n) const { std::string ret = "unnamed"; diff --git a/lib/GameConstants.h b/lib/GameConstants.h index 4d48ec186..7fa93a4e5 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -257,12 +257,14 @@ class PlayerColor : public BaseForID PLAYER_LIMIT_I = 8 }; + DLL_LINKAGE static const PlayerColor SPECTATOR; //252 DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253 DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) DLL_LINKAGE static const PlayerColor NEUTRAL; //255 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 isSpectator() const; DLL_LINKAGE std::string getStr(bool L10n = false) const; DLL_LINKAGE std::string getStrCap(bool L10n = false) const; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 61992ae09..0268c5727 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1842,6 +1842,9 @@ DLL_LINKAGE void BattleSetStackProperty::applyGs(CGameState *gs) DLL_LINKAGE void PlayerCheated::applyGs(CGameState *gs) { + if(!player.isValidPlayer()) + return; + gs->getPlayer(player)->enteredLosingCheatCode = losingCheatCode; gs->getPlayer(player)->enteredWinningCheatCode = winningCheatCode; } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index bf5161fa6..67dfbc6a4 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1878,7 +1878,8 @@ void CGameHandler::run(bool resume) sbuffer << color << " "; { boost::unique_lock lock(gsm); - connections[color] = cc; + if(!color.isSpectator()) // there can be more than one spectator + connections[color] = cc; } } 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)); 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 } } diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 956eb0220..b5ed4d514 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -283,8 +283,11 @@ bool CastAdvSpell::applyGh( CGameHandler *gh ) bool PlayerMessage::applyGh( CGameHandler *gh ) { - ERROR_IF_NOT(player); - if(gh->getPlayerAt(c) != player) ERROR_AND_RETURN; + if(!player.isSpectator()) // TODO: clearly not a great way to verify permissions + { + ERROR_IF_NOT(player); + if(gh->getPlayerAt(c) != player) ERROR_AND_RETURN; + } gh->playerMessage(player,text, currObj); return true; }