/* * CMapGenOptions.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CMapGenOptions.h" #include "../mapping/CMapHeader.h" #include "CRmgTemplateStorage.h" #include "CRmgTemplate.h" #include "CRandomGenerator.h" #include "../VCMI_Lib.h" #include "../CTownHandler.h" VCMI_LIB_NAMESPACE_BEGIN CMapGenOptions::CMapGenOptions() : width(CMapHeader::MAP_SIZE_MIDDLE), height(CMapHeader::MAP_SIZE_MIDDLE), hasTwoLevels(true), humanOrCpuPlayerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr), customizedPlayers(false) { initPlayersMap(); setRoadEnabled(RoadId(Road::DIRT_ROAD), true); setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true); setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true); } si32 CMapGenOptions::getWidth() const { return width; } void CMapGenOptions::setWidth(si32 value) { assert(value >= 1); width = value; } si32 CMapGenOptions::getHeight() const { return height; } void CMapGenOptions::setHeight(si32 value) { assert(value >= 1); height = value; } bool CMapGenOptions::getHasTwoLevels() const { return hasTwoLevels; } void CMapGenOptions::setHasTwoLevels(bool value) { hasTwoLevels = value; } si8 CMapGenOptions::getHumanOrCpuPlayerCount() const { return humanOrCpuPlayerCount; } void CMapGenOptions::setHumanOrCpuPlayerCount(si8 value) { // Set total player count (human + AI)? assert((value >= 1 && value <= PlayerColor::PLAYER_LIMIT_I) || value == RANDOM_SIZE); humanOrCpuPlayerCount = value; // Use template player limit, if any auto playerLimit = getPlayerLimit(); auto possibleCompPlayersCount = playerLimit - std::max(0, humanOrCpuPlayerCount); if (compOnlyPlayerCount > possibleCompPlayersCount) { setCompOnlyPlayerCount(possibleCompPlayersCount); } resetPlayersMap(); } si8 CMapGenOptions::getTotalPlayersCount() const { auto totalPlayers = 0; si8 humans = getHumanOrCpuPlayerCount(); si8 cpus = getCompOnlyPlayerCount(); if (humans == RANDOM_SIZE || cpus == RANDOM_SIZE) { totalPlayers = PlayerColor::PLAYER_LIMIT_I; } else { totalPlayers = humans + cpus; } assert (totalPlayers <= PlayerColor::PLAYER_LIMIT_I); assert (totalPlayers >= 2); return totalPlayers; } si8 CMapGenOptions::getTeamCount() const { return teamCount; } void CMapGenOptions::setTeamCount(si8 value) { assert(getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value < getHumanOrCpuPlayerCount()) || value == RANDOM_SIZE); teamCount = value; } si8 CMapGenOptions::getCompOnlyPlayerCount() const { return compOnlyPlayerCount; } si8 CMapGenOptions::getPlayerLimit() const { si8 playerLimit = PlayerColor::PLAYER_LIMIT_I; if (auto temp = getMapTemplate()) { playerLimit = *boost::max_element(temp->getPlayers().getNumbers()); } return playerLimit; } void CMapGenOptions::setCompOnlyPlayerCount(si8 value) { auto playerLimit = getPlayerLimit(); assert(value == RANDOM_SIZE || (getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= playerLimit - getHumanOrCpuPlayerCount()))); compOnlyPlayerCount = value; resetPlayersMap(); } si8 CMapGenOptions::getCompOnlyTeamCount() const { return compOnlyTeamCount; } void CMapGenOptions::setCompOnlyTeamCount(si8 value) { assert(value == RANDOM_SIZE || compOnlyPlayerCount == RANDOM_SIZE || (value >= 0 && value <= std::max(compOnlyPlayerCount - 1, 0))); compOnlyTeamCount = value; } EWaterContent::EWaterContent CMapGenOptions::getWaterContent() const { return waterContent; } void CMapGenOptions::setWaterContent(EWaterContent::EWaterContent value) { waterContent = value; } EMonsterStrength::EMonsterStrength CMapGenOptions::getMonsterStrength() const { return monsterStrength; } void CMapGenOptions::setMonsterStrength(EMonsterStrength::EMonsterStrength value) { monsterStrength = value; } void CMapGenOptions::initPlayersMap() { players.clear(); int realPlayersCnt = getHumanOrCpuPlayerCount(); // TODO: Initialize settings for all color even if not present? for(int color = 0; color < getPlayerLimit(); ++color) { CPlayerSettings player; auto pc = PlayerColor(color); player.setColor(pc); /* if (vstd::contains(savedPlayerSettings, pc)) { player.setTeam(savedPlayerSettings[pc].getTeam()); player.setStartingTown(savedPlayerSettings[pc].getStartingTown()); //TODO: Restore starting hero and bonus? } // Assign new owner of this player */ auto playerType = EPlayerType::AI; // Color doesn't have to be continuous. Player colors can later be changed manually if (getHumanOrCpuPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) { playerType = EPlayerType::HUMAN; } else if((getHumanOrCpuPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I - compOnlyPlayerCount))) { playerType = EPlayerType::COMP_ONLY; } player.setPlayerType(playerType); players[pc] = player; } savePlayersMap(); } void CMapGenOptions::resetPlayersMap() { // Called when number of player changes // TODO: Should it? //But do not update info about already made selections savePlayersMap(); /* //Remove players who have undefined properties vstd::erase_if(players, [](const std::pair & p) { return p.second.getPlayerType() != EPlayerType::AI && p.second.getStartingTown() == CPlayerSettings::RANDOM_TOWN; }); */ // FIXME: This should be total players count int realPlayersCnt = getHumanOrCpuPlayerCount(); if (realPlayersCnt != RANDOM_SIZE) { //Trim the number of AI players, then CPU-only players, finally human players auto eraseLastPlayer = [this](EPlayerType playerType) -> bool { //FIXME: Infinite loop for 0 players for (auto it = players.rbegin(); it != players.rend(); ++it) { if (it->second.getPlayerType() == playerType) { players.erase(it->first); return true; } } return false; //Can't earse any player of this type }; while (players.size() > realPlayersCnt) { while (eraseLastPlayer(EPlayerType::AI)); while (eraseLastPlayer(EPlayerType::COMP_ONLY)); while (eraseLastPlayer(EPlayerType::HUMAN)); } } else { //If count is random, generate info for all the players realPlayersCnt = PlayerColor::PLAYER_LIMIT_I; } int realCompOnlyPlayersCnt = getCompOnlyPlayerCount(); //int totalPlayersLimit = getPlayerLimit(); //First colors from the list are assigned to human players, then to CPU players std::vector availableColors; for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; color++) { availableColors.push_back(PlayerColor(color)); } auto removeUsedColors = [this, &availableColors](EPlayerType playerType) { for (auto& player : players) { if (player.second.getPlayerType() == playerType) { vstd::erase(availableColors, player.second.getColor()); //FIXME: Where is this color initialized at lobby launch? } } }; removeUsedColors(EPlayerType::HUMAN); removeUsedColors(EPlayerType::COMP_ONLY); //removeUsedColors(EPlayerType::AI); //Assign unused colors to remaining AI players while (players.size() < realPlayersCnt && !availableColors.empty()) { auto color = availableColors.front(); setPlayerTypeForStandardPlayer(color, EPlayerType::AI); players[color].setColor(color); availableColors.erase(availableColors.begin()); if (vstd::contains(savedPlayerSettings, color)) { setPlayerTeam(color, savedPlayerSettings.at(color).getTeam()); // TODO: setter players[color].setStartingTown(savedPlayerSettings.at(color).getStartingTown()); } else { logGlobal->warn("Adding settings for player %s", color.encode(color)); // Usually, all players should be initialized in initPlayersMap() CPlayerSettings settings; players[color] = settings; } } // TODO: Assign players to teams at the beginning (if all players belong to the same team) std::set occupiedTeams; for(auto & player : players) { auto team = player.second.getTeam(); if (team != TeamID::NO_TEAM) { occupiedTeams.insert(team); } } // TODO: Handle situation when we remove a player and remaining players belong to only one team for(auto & player : players) { if (player.second.getTeam() == TeamID::NO_TEAM) { //Find first unused team for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { TeamID team(i); if(!occupiedTeams.count(team)) { player.second.setTeam(team); occupiedTeams.insert(team); break; } } } } /* for(int color = 0; color < totalPlayersLimit; ++color) { CPlayerSettings player; auto pc = PlayerColor(color); player.setColor(pc); auto playerType = EPlayerType::AI; if (getPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) { playerType = EPlayerType::HUMAN; } else if((getPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I-compOnlyPlayerCount))) { //FIXME: Allow humans to choose any color, even from the end of the list playerType = EPlayerType::COMP_ONLY; } player.setPlayerType(playerType); player.setTeam(rememberTeam[pc]); players[pc] = player; if (vstd::contains(rememberTownTypes, pc)) players[pc].setStartingTown(rememberTownTypes[pc]); } */ } void CMapGenOptions::savePlayersMap() { //Only save already configured players for (const auto& player : players) { savedPlayerSettings[player.first] = player.second; } } const std::map & CMapGenOptions::getSavedPlayersMap() const { return savedPlayerSettings; } const std::map & CMapGenOptions::getPlayersSettings() const { return players; } void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, si32 town) { auto it = players.find(color); assert(it != players.end()); it->second.setStartingTown(town); } void CMapGenOptions::setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType) { // FIXME: Why actually not set it to COMP_ONLY? Ie. when swapping human to another color? assert(playerType != EPlayerType::COMP_ONLY); auto it = players.find(color); assert(it != players.end()); it->second.setPlayerType(playerType); customizedPlayers = true; } const CRmgTemplate * CMapGenOptions::getMapTemplate() const { return mapTemplate; } void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) { mapTemplate = value; //validate & adapt options according to template if(mapTemplate) { if(!mapTemplate->matchesSize(int3(getWidth(), getHeight(), 1 + getHasTwoLevels()))) { auto sizes = mapTemplate->getMapSizes(); setWidth(sizes.first.x); setHeight(sizes.first.y); setHasTwoLevels(sizes.first.z - 1); } if(!mapTemplate->getPlayers().isInRange(getHumanOrCpuPlayerCount())) setHumanOrCpuPlayerCount(RANDOM_SIZE); if(!mapTemplate->getCpuPlayers().isInRange(getCompOnlyPlayerCount())) setCompOnlyPlayerCount(RANDOM_SIZE); if(!mapTemplate->getWaterContentAllowed().count(getWaterContent())) setWaterContent(EWaterContent::RANDOM); } } void CMapGenOptions::setMapTemplate(const std::string & name) { if(!name.empty()) setMapTemplate(VLC->tplh->getTemplate(name)); } void CMapGenOptions::setRoadEnabled(const RoadId & roadType, bool enable) { if (enable) { enabledRoads.insert(roadType); } else { enabledRoads.erase(roadType); } } bool CMapGenOptions::isRoadEnabled(const RoadId & roadType) const { return enabledRoads.count(roadType); } bool CMapGenOptions::isRoadEnabled() const { return !enabledRoads.empty(); } void CMapGenOptions::setPlayerTeam(const PlayerColor & color, const TeamID & team) { auto it = players.find(color); assert(it != players.end()); // TODO: Make pivate friend method to avoid unintended changes? it->second.setTeam(team); customizedPlayers = true; } void CMapGenOptions::finalize(CRandomGenerator & rand) { logGlobal->info("RMG map: %dx%d, %s underground", getWidth(), getHeight(), getHasTwoLevels() ? "WITH" : "NO"); logGlobal->info("RMG settings: players %d, teams %d, computer players %d, computer teams %d, water %d, monsters %d", static_cast(getHumanOrCpuPlayerCount()), static_cast(getTeamCount()), static_cast(getCompOnlyPlayerCount()), static_cast(getCompOnlyTeamCount()), static_cast(getWaterContent()), static_cast(getMonsterStrength())); if(!mapTemplate) { mapTemplate = getPossibleTemplate(rand); } assert(mapTemplate); logGlobal->info("RMG template name: %s", mapTemplate->getName()); if (getHumanOrCpuPlayerCount() == RANDOM_SIZE) { auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); //ignore all non-randomized players, make sure these players will not be missing after roll possiblePlayers.erase(possiblePlayers.begin(), possiblePlayers.lower_bound(countHumanPlayers() + countCompOnlyPlayers())); assert(!possiblePlayers.empty()); setHumanOrCpuPlayerCount (*RandomGeneratorUtil::nextItem(possiblePlayers, rand)); updatePlayers(); } if(teamCount == RANDOM_SIZE) { teamCount = rand.nextInt(getHumanOrCpuPlayerCount() - 1); if (teamCount == 1) teamCount = 0; } if(compOnlyPlayerCount == RANDOM_SIZE) { auto possiblePlayers = mapTemplate->getCpuPlayers().getNumbers(); compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); updateCompOnlyPlayers(); } if(compOnlyTeamCount == RANDOM_SIZE) { compOnlyTeamCount = rand.nextInt(std::max(compOnlyPlayerCount - 1, 0)); } if(waterContent == EWaterContent::RANDOM) { auto allowedContent = mapTemplate->getWaterContentAllowed(); if(!allowedContent.empty()) { waterContent = *RandomGeneratorUtil::nextItem(mapTemplate->getWaterContentAllowed(), rand); } else { waterContent = EWaterContent::NONE; } } if(monsterStrength == EMonsterStrength::RANDOM) { monsterStrength = static_cast(rand.nextInt(EMonsterStrength::GLOBAL_WEAK, EMonsterStrength::GLOBAL_STRONG)); } assert (vstd::iswithin(waterContent, EWaterContent::NONE, EWaterContent::ISLANDS)); assert (vstd::iswithin(monsterStrength, EMonsterStrength::GLOBAL_WEAK, EMonsterStrength::GLOBAL_STRONG)); logGlobal->trace("Player config:"); int cpuOnlyPlayers = 0; for(const auto & player : players) { std::string playerType; switch (player.second.getPlayerType()) { case EPlayerType::AI: playerType = "AI"; break; case EPlayerType::COMP_ONLY: playerType = "computer only"; cpuOnlyPlayers++; break; case EPlayerType::HUMAN: playerType = "human only"; break; default: assert(false); } // FIXME: Every player is player 0 with type of AI // FIXME: player.first != player.second.getColor() // TODO: Set player color everywhere players is set, or only once here logGlobal->trace("Player %d: %s", player.second.getColor(), playerType); } // FXIME: Do not set this again after options were set setCompOnlyPlayerCount(cpuOnlyPlayers); //human players are set automatically (?) logGlobal->info("Final player config: %d total, %d cpu-only", players.size(), static_cast(getCompOnlyPlayerCount())); } void CMapGenOptions::updatePlayers() { // Remove non-human players only from the end of the players map if necessary for(auto itrev = players.end(); itrev != players.begin();) { auto it = itrev; --it; if (players.size() == getHumanOrCpuPlayerCount()) break; if(it->second.getPlayerType() != EPlayerType::HUMAN) { players.erase(it); } else { --itrev; } } } void CMapGenOptions::updateCompOnlyPlayers() { // Remove comp only players only from the end of the players map if necessary for(auto itrev = players.end(); itrev != players.begin();) { auto it = itrev; --it; if (players.size() <= getHumanOrCpuPlayerCount()) break; if(it->second.getPlayerType() == EPlayerType::COMP_ONLY) { players.erase(it); } else { --itrev; } } // Add some comp only players if necessary int compOnlyPlayersToAdd = static_cast(getHumanOrCpuPlayerCount() - players.size()); if (compOnlyPlayersToAdd < 0) { logGlobal->error("Incorrect number of players to add. Requested players %d, current players %d", humanOrCpuPlayerCount, players.size()); assert (compOnlyPlayersToAdd < 0); } for(int i = 0; i < compOnlyPlayersToAdd; ++i) { CPlayerSettings pSettings; pSettings.setPlayerType(EPlayerType::COMP_ONLY); pSettings.setColor(getNextPlayerColor()); players[pSettings.getColor()] = pSettings; } } int CMapGenOptions::countHumanPlayers() const { return static_cast(boost::count_if(players, [](const std::pair & pair) { return pair.second.getPlayerType() == EPlayerType::HUMAN; })); } int CMapGenOptions::countCompOnlyPlayers() const { return static_cast(boost::count_if(players, [](const std::pair & pair) { return pair.second.getPlayerType() == EPlayerType::COMP_ONLY; })); } PlayerColor CMapGenOptions::getNextPlayerColor() const { for(PlayerColor i = PlayerColor(0); i < PlayerColor::PLAYER_LIMIT; i.advance(1)) { if(!players.count(i)) { return i; } } logGlobal->error("Failed to get next player color"); assert(false); return PlayerColor(0); } bool CMapGenOptions::checkOptions() const { if(mapTemplate) { return true; } else { CRandomGenerator gen; return getPossibleTemplate(gen) != nullptr; } } bool CMapGenOptions::arePlayersCustomized() const { return customizedPlayers; } std::vector CMapGenOptions::getPossibleTemplates() const { int3 tplSize(width, height, (hasTwoLevels ? 2 : 1)); auto humanPlayers = countHumanPlayers(); auto templates = VLC->tplh->getTemplates(); vstd::erase_if(templates, [this, &tplSize, humanPlayers](const CRmgTemplate * tmpl) { if(!tmpl->matchesSize(tplSize)) return true; if(!tmpl->isWaterContentAllowed(getWaterContent())) return true; if(getHumanOrCpuPlayerCount() != CMapGenOptions::RANDOM_SIZE) { if (!tmpl->getPlayers().isInRange(getHumanOrCpuPlayerCount())) return true; } else { // Human players shouldn't be banned when playing with random player count if(humanPlayers > *boost::min_element(tmpl->getPlayers().getNumbers())) return true; } if(compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) { if (!tmpl->getCpuPlayers().isInRange(compOnlyPlayerCount)) return true; } return false; }); return templates; } const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & rand) const { auto templates = getPossibleTemplates(); if(templates.empty()) return nullptr; return *RandomGeneratorUtil::nextItem(templates, rand); } CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) { } PlayerColor CMapGenOptions::CPlayerSettings::getColor() const { return color; } void CMapGenOptions::CPlayerSettings::setColor(const PlayerColor & value) { assert(value >= PlayerColor(0) && value < PlayerColor::PLAYER_LIMIT); color = value; } si32 CMapGenOptions::CPlayerSettings::getStartingTown() const { return startingTown; } void CMapGenOptions::CPlayerSettings::setStartingTown(si32 value) { assert(value >= -1); if(value >= 0) { assert(value < static_cast(VLC->townh->size())); assert((*VLC->townh)[value]->town != nullptr); } startingTown = value; } EPlayerType CMapGenOptions::CPlayerSettings::getPlayerType() const { return playerType; } void CMapGenOptions::CPlayerSettings::setPlayerType(EPlayerType value) { playerType = value; } TeamID CMapGenOptions::CPlayerSettings::getTeam() const { return team; } void CMapGenOptions::CPlayerSettings::setTeam(const TeamID & value) { team = value; } VCMI_LIB_NAMESPACE_END