Merge pull request #284 from cbeck88/autodownload_addons

automatically download needed addons in mp lobby
This commit is contained in:
Chris Beck 2015-03-15 13:40:02 -04:00
commit 844dd5d799
11 changed files with 862 additions and 359 deletions

View file

@ -116,9 +116,45 @@ std::string describe_addon_status(const addon_tracking_info& info)
}
}
/** Warns the user about unresolved dependencies and installs them if they choose to do so. */
bool do_resolve_addon_dependencies(display& disp, addons_client& client, const addons_list& addons, const addon_info& addon, bool& wml_changed)
// Asks the client to download and install an addon, reporting errors in a gui dialog. Returns true if new content was installed, false otherwise.
static bool try_fetch_addon(display & disp, addons_client & client, const addon_info & addon)
{
config archive;
if(!(
client.download_addon(archive, addon.id, addon.title, !is_addon_installed(addon.id)) &&
client.install_addon(archive, addon)
)) {
const std::string& server_error = client.get_last_server_error();
if(!server_error.empty()) {
gui2::show_error_message(disp.video(),
std::string(_("The server responded with an error:")) + "\n" + server_error);
}
return false;
} else {
return true;
}
}
enum OUTCOME { SUCCESS, FAILURE, ABORT };
// A structure which summarizes the outcome of one or more add-on install operations.
struct ADDON_OP_RESULT {
OUTCOME outcome_;
bool wml_changed_;
};
/** Warns the user about unresolved dependencies and installs them if they choose to do so.
* Returns: outcome: ABORT in case the user chose to abort because of an issue
* SUCCESS otherwise
* wml_change: indicates if new wml content was installed
*/
ADDON_OP_RESULT do_resolve_addon_dependencies(display& disp, addons_client& client, const addons_list& addons, const addon_info& addon)
{
ADDON_OP_RESULT result;
result.outcome_ = SUCCESS;
result.wml_changed_ = false;
boost::scoped_ptr<cursor::setter> cursor_setter(new cursor::setter(cursor::WAIT));
// TODO: We don't currently check for the need to upgrade. I'll probably
@ -155,13 +191,14 @@ bool do_resolve_addon_dependencies(display& disp, addons_client& client, const a
}
if(gui2::show_message(disp.video(), _("Broken Dependencies"), broken_deps_report, gui2::tmessage::yes_no_buttons) != gui2::twindow::OK) {
return false; // canceled by user
result.outcome_ = ABORT;
return result; // canceled by user
}
}
if(missing_deps.empty()) {
// No dependencies to install, carry on.
return true;
return result;
}
//
@ -216,7 +253,7 @@ bool do_resolve_addon_dependencies(display& disp, addons_client& client, const a
cursor_setter.reset();
if(dlg.show() < 0) {
return true;
return result; // the user has chosen to continue without installing anything.
}
}
@ -229,21 +266,10 @@ bool do_resolve_addon_dependencies(display& disp, addons_client& client, const a
BOOST_FOREACH(const std::string& dep, missing_deps) {
const addon_info& addon = addon_at(dep, addons);
config archive;
if(!(
client.download_addon(archive, addon.id, addon.title, !is_addon_installed(addon.id)) &&
client.install_addon(archive, addon)
)) {
const std::string& server_error = client.get_last_server_error();
if(!server_error.empty()) {
gui2::show_error_message(disp.video(),
std::string(_("The server responded with an error:")) + "\n" + server_error);
}
if(!try_fetch_addon(disp, client, addon)) {
failed_titles.push_back(addon.title);
} else {
wml_changed = true;
result.wml_changed_ = true;
}
}
@ -253,10 +279,11 @@ bool do_resolve_addon_dependencies(display& disp, addons_client& client, const a
"The following dependencies could not be installed. Do you still wish to continue?",
failed_titles.size()) + std::string("\n\n") + utils::bullet_list(failed_titles);
return gui2::show_message(disp.video(), _("Dependencies Installation Failed"), failed_deps_report, gui2::tmessage::yes_no_buttons) == gui2::twindow::OK;
result.outcome_ = gui2::show_message(disp.video(), _("Dependencies Installation Failed"), failed_deps_report, gui2::tmessage::yes_no_buttons) == gui2::twindow::OK ? SUCCESS : ABORT; // If the user cancels, return ABORT. Otherwise, return SUCCESS, since the user chose to ignore the failure.
return result;
}
return true;
return result;
}
/** Checks whether the given add-on has local .pbl or VCS information and asks before overwriting it. */
@ -293,6 +320,38 @@ bool do_check_before_overwriting_addon(CVideo& video, const addon_info& addon)
return gui2::show_message(video, _("Confirm"), text, gui2::tmessage::yes_no_buttons) == gui2::twindow::OK;
}
/** Do a 'smart' fetch of an add-on, checking to avoid overwrites for devs and resolving dependencies, using gui interaction to handle issues that arise
* Returns: outcome: ABORT in case the user chose to abort because of an issue
* FAILURE in case we resolved checks and dependencies, but fetching this particular add-on failed
* SUCCESS otherwise
* wml_changed: indicates if new wml content was installed at any point
*/
static ADDON_OP_RESULT try_fetch_addon_with_checks(display & disp, addons_client& client, const addons_list& addons, const addon_info& addon)
{
if(!(do_check_before_overwriting_addon(disp.video(), addon))) {
// Just do nothing and leave.
ADDON_OP_RESULT result;
result.outcome_ = ABORT;
result.wml_changed_ = false;
return result;
}
// Resolve any dependencies
ADDON_OP_RESULT res = do_resolve_addon_dependencies(disp, client, addons, addon);
if (res.outcome_ != SUCCESS) { // this function only returns SUCCESS and ABORT as outcomes
return res; // user aborted
}
if(!try_fetch_addon(disp, client, addon)) {
res.outcome_ = FAILURE;
return res; //wml_changed should have whatever value was obtained in resolving dependencies
} else {
res.wml_changed_ = true;
return res; //we successfully installed something, so now the wml was definitely changed
}
}
/** Performs all backend and UI actions for taking down the specified add-on. */
void do_remote_addon_delete(CVideo& video, addons_client& client, const std::string& addon_id)
{
@ -866,24 +925,13 @@ void show_addons_manager_dialog(display& disp, addons_client& client, addons_lis
BOOST_FOREACH(const std::string& id, ids_to_install) {
const addon_info& addon = addon_at(id, addons);
if(!(do_check_before_overwriting_addon(disp.video(), addon) && do_resolve_addon_dependencies(disp, client, addons, addon, wml_changed))) {
// Just do nothing and leave.
return;
}
config archive;
if(!(
client.download_addon(archive, addon.id, addon.title, !is_addon_installed(addon.id)) &&
client.install_addon(archive, addon)
)) {
failed_titles.push_back(addon.title);
const std::string& server_error = client.get_last_server_error();
if(!server_error.empty()) {
gui2::show_error_message(disp.video(),
std::string(_("The server responded with an error:")) + "\n" + server_error);
}
} else {
ADDON_OP_RESULT res = try_fetch_addon_with_checks(disp, client, addons, addon);
wml_changed |= res.wml_changed_; // take note if any wml_changes occurred
if (res.outcome_ == ABORT) {
return; // the user aborted because of some issue encountered
} else if (res.outcome_ == FAILURE) {
failed_titles.push_back(addon.title); // we resolved dependencies, but fetching this particular addon failed.
} else { // res.outcome == SUCCESS
wml_changed = true;
}
}
@ -1159,3 +1207,62 @@ bool manage_addons(display& disp)
return false;
}
}
bool ad_hoc_addon_fetch_session(display & disp, const std::vector<std::string> & addon_ids)
{
std::string remote_address = preferences::campaign_server();
// These exception handlers copied from addon_manager_ui fcn above.
try {
addons_client client(disp, remote_address);
client.connect();
addons_list addons;
if(!get_addons_list(client, addons)) {
gui2::show_error_message(disp.video(), _("An error occurred while downloading the add-ons list from the server."));
return false;
}
bool return_value = true;
BOOST_FOREACH(const std::string & addon_id, addon_ids) {
const addon_info& addon = addon_at(addon_id, addons);
ADDON_OP_RESULT res = try_fetch_addon_with_checks(disp, client, addons, addon);
return_value = return_value && (res.outcome_ == SUCCESS);
}
return return_value;
} catch(const config::error& e) {
ERR_CFG << "config::error thrown during transaction with add-on server; \""<< e.message << "\"" << std::endl;
gui2::show_error_message(disp.video(), _("Network communication error."));
} catch(const network::error& e) {
ERR_NET << "network::error thrown during transaction with add-on server; \""<< e.message << "\"" << std::endl;
gui2::show_error_message(disp.video(), _("Remote host disconnected."));
} catch(const network_asio::error& e) {
ERR_NET << "network_asio::error thrown during transaction with add-on server; \""<< e.what() << "\"" << std::endl;
gui2::show_error_message(disp.video(), _("Remote host disconnected."));
} catch(const filesystem::io_exception& e) {
ERR_FS << "io_exception thrown while installing an addon; \"" << e.what() << "\"" << std::endl;
gui2::show_error_message(disp.video(), _("A problem occurred when trying to create the files necessary to install this add-on."));
} catch(const invalid_pbl_exception& e) {
ERR_CFG << "could not read .pbl file " << e.path << ": " << e.message << std::endl;
utils::string_map symbols;
symbols["path"] = e.path;
symbols["msg"] = e.message;
gui2::show_error_message(disp.video(),
vgettext("A local file with add-on publishing information could not be read.\n\nFile: $path\nError message: $msg", symbols));
} catch(twml_exception& e) {
e.show(disp);
} catch(const addons_client::user_exit&) {
LOG_AC << "initial connection canceled by user\n";
} catch(const addons_client::invalid_server_address&) {
gui2::show_error_message(disp.video(), _("The add-ons server address specified is not valid."));
}
return false;
}

View file

@ -17,6 +17,7 @@
#define ADDON_MANAGER_UI_HPP_INCLUDED
#include <string>
#include <vector>
class display;
@ -30,4 +31,15 @@ class display;
*/
bool manage_addons(display& disp);
/**
* Conducts an ad-hoc add-ons server connection to download an add-on with a particular id and all
* it's dependencies. Launches gui dialogs when issues arise.
*
* @param disp Display object on which to render UI elements.
* @param id The id of the target add-on.
*
* @return @a true when we successfully installed the target (possibly the user chose to ignore failures)
*/
bool ad_hoc_addon_fetch_session(display & disp, const std::vector<std::string> & addon_ids);
#endif

View file

@ -31,6 +31,7 @@
#include "terrain_builder.hpp"
#include "terrain_type_data.hpp"
#include "unit_types.hpp"
#include "version.hpp"
#include <boost/foreach.hpp>
#include <boost/make_shared.hpp>
@ -316,6 +317,12 @@ void game_config_manager::load_game_config(FORCE_RELOAD_CONFIG force_reload,
paths_manager_.set_paths(game_config());
}
struct addon_source {
std::string main_cfg;
std::string addon_id;
version_info version;
};
void game_config_manager::load_addons_cfg()
{
const std::string user_campaign_dir = filesystem::get_addons_dir();
@ -323,7 +330,7 @@ void game_config_manager::load_addons_cfg()
std::vector<std::string> error_addons;
std::vector<std::string> user_dirs;
std::vector<std::string> user_files;
std::vector<std::string> addons_to_load;
std::vector<addon_source> addons_to_load;
filesystem::get_files_in_dir(user_campaign_dir, &user_files, &user_dirs,
filesystem::ENTIRE_FILE_PATH);
@ -335,86 +342,93 @@ void game_config_manager::load_addons_cfg()
const std::string file = uc;
const int size_minus_extension = file.size() - 4;
if(file.substr(size_minus_extension, file.size()) == ".cfg") {
bool ok = true;
// Allowing it if the dir doesn't exist,
// for the single-file add-on.
if(filesystem::file_exists(file.substr(0, size_minus_extension))) {
// Unfortunately, we create the dir plus
// _info.cfg ourselves on download.
std::vector<std::string> dirs, files;
filesystem::get_files_in_dir(file.substr(0, size_minus_extension),
&files, &dirs);
if(dirs.size() > 0) {
ok = false;
}
if(files.size() > 1) {
ok = false;
}
if(files.size() == 1 && files[0] != "_info.cfg") {
ok = false;
}
}
if(!ok) {
const int userdata_loc = file.find("data/add-ons") + 5;
ERR_CONFIG << "error reading usermade add-on '"
<< file << "'\n";
error_addons.push_back(file);
error_log.push_back("The format '~" + file.substr(userdata_loc)
+ "' is only for single-file add-ons, use '~"
+ "' (for single-file add-ons) is not supported anymore, use '~"
+ file.substr(userdata_loc,
size_minus_extension - userdata_loc)
+ "/_main.cfg' instead.");
}
else {
addons_to_load.push_back(file);
}
}
}
// Rerun the directory scan using filename only, to get the addon_ids more easily.
user_files.clear();
user_dirs.clear();
filesystem::get_files_in_dir(user_campaign_dir, &user_files, &user_dirs,
filesystem::FILE_NAME_ONLY);
// Append the $user_campaign_dir/*/_main.cfg files to addons_to_load.
BOOST_FOREACH(const std::string& uc, user_dirs) {
const std::string addon_id = uc;
const std::string addon_dir = user_campaign_dir + "/" + uc;
const std::string info_cfg = uc + "/_info.cfg";
if (filesystem::file_exists(info_cfg)) {
const std::string main_cfg = addon_dir + "/_main.cfg";
const std::string info_cfg = addon_dir + "/_info.cfg";
config info;
cache_.get_config(info_cfg, info);
const config info_tag = info.child_or_empty("info");
std::string core = info_tag["core"];
if (core.empty()) core = "default";
if ( !info_tag.empty() && // Don't skip addons which have no [info], they are most likely manually installed.
info_tag["type"] != "core" && // Don't skip cores, we want them selectable at all times.
core != preferences::core_id() // Don't skip addons matching our current core.
)
continue; // Skip add-ons not matching our current core.
}
addon_source addon;
addon.main_cfg = main_cfg;
addon.addon_id = addon_id;
const std::string main_cfg = uc + "/_main.cfg";
if(filesystem::file_exists(main_cfg)) {
addons_to_load.push_back(main_cfg);
if (filesystem::file_exists(main_cfg)) {
if (filesystem::file_exists(info_cfg)) {
config info;
cache_.get_config(info_cfg, info);
const config info_tag = info.child_or_empty("info");
std::string core = info_tag["core"];
if (core.empty()) core = "default";
if ( !info_tag.empty() && // Don't skip addons which have no [info], they are most likely manually installed.
info_tag["type"] != "core" && // Don't skip cores, we want them selectable at all times.
core != preferences::core_id() // Don't skip addons matching our current core.
) {
continue; // Skip add-ons not matching our current core.
}
}
// Ask the addon manager to find version info for us (from info, pbl file)
addon.version = get_addon_version_info(addon_id);
addons_to_load.push_back(addon);
}
}
// Load the addons.
BOOST_FOREACH(const std::string& uc, addons_to_load) {
const std::string toplevel = uc;
BOOST_FOREACH(const addon_source & addon, addons_to_load) {
try {
// Load this addon from the cache, to a config
config umc_cfg;
cache_.get_config(toplevel, umc_cfg);
cache_.get_config(addon.main_cfg, umc_cfg);
// Annotate "era" and "modification" tags with addon_id info
const char * tags_with_addon_id [] = { "era", "modification", NULL };
for (const char ** type = tags_with_addon_id; *type; type++)
{
BOOST_FOREACH(config & cfg, umc_cfg.child_range(*type)) {
cfg["addon_id"] = addon.addon_id;
if (addon.version.good()) {
// If the addon string was not "sane" then reject it, we can't compare non-sane version strings.
// This may also happen if no version info could be found.
cfg["addon_version"] = addon.version.str(); // Note that this may reformat the string in a canonical form.
}
}
}
game_config_.append(umc_cfg);
} catch(config::error& err) {
ERR_CONFIG << "error reading usermade add-on '" << uc << "'" << std::endl;
ERR_CONFIG << "error reading usermade add-on '" << addon.main_cfg << "'" << std::endl;
ERR_CONFIG << err.message << '\n';
error_addons.push_back(uc);
error_addons.push_back(addon.main_cfg);
error_log.push_back(err.message);
} catch(preproc_config::error& err) {
ERR_CONFIG << "error reading usermade add-on '" << uc << "'" << std::endl;
ERR_CONFIG << "error reading usermade add-on '" << addon.main_cfg << "'" << std::endl;
ERR_CONFIG << err.message << '\n';
error_addons.push_back(uc);
error_addons.push_back(addon.main_cfg);
error_log.push_back(err.message);
} catch(filesystem::io_exception&) {
ERR_CONFIG << "error reading usermade add-on '" << uc << "'" << std::endl;
error_addons.push_back(uc);
ERR_CONFIG << "error reading usermade add-on '" << addon.main_cfg << "'" << std::endl;
error_addons.push_back(addon.main_cfg);
}
}
if(error_addons.empty() == false) {

View file

@ -0,0 +1,28 @@
/*
Copyright (C) 2015 by Chris Beck <render787@gmail.com>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#ifndef LOBBY_RELOAD_REQUEST_EXCEPTION_HPP_INCLUDED
#define LOBBY_RELOAD_REQUEST_EXCEPTION_HPP_INCLUDED
#include <exception>
#include <string>
namespace mp {
struct lobby_reload_request_exception : game::error {
lobby_reload_request_exception() : game::error("Lobby requested to reload the game config and launch again") {}
};
}
#endif

View file

@ -13,6 +13,7 @@
*/
#include "multiplayer.hpp"
#include "addon/manager.hpp" // for get_installed_addons
#include "dialogs.hpp"
#include "formula_string_utils.hpp"
#include "game_preferences.hpp"
@ -27,6 +28,7 @@
#include "gui/widgets/settings.hpp"
#include "gui/widgets/window.hpp"
#include "hash.hpp"
#include "lobby_reload_request_exception.hpp"
#include "log.hpp"
#include "generators/map_create.hpp"
#include "mp_game_utils.hpp"
@ -646,7 +648,7 @@ static void do_preferences_dialog(game_display& disp, const config& game_config)
}
static void enter_lobby_mode(game_display& disp, const config& game_config,
saved_game& state)
saved_game& state, const std::vector<std::string> & installed_addons)
{
DBG_MP << "entering lobby mode" << std::endl;
@ -695,7 +697,7 @@ static void enter_lobby_mode(game_display& disp, const config& game_config,
res = mp::ui::QUIT;
}
} else {
mp::lobby ui(disp, game_config, gamechat, gamelist);
mp::lobby ui(disp, game_config, gamechat, gamelist, installed_addons);
run_lobby_loop(disp, ui);
res = ui.get_result();
}
@ -904,6 +906,12 @@ void start_local_game_commandline(game_display& disp, const config& game_config,
void start_client(game_display& disp, const config& game_config,
saved_game& state, const std::string& host)
{
const config * game_config_ptr = &game_config;
std::vector<std::string> installed_addons = ::installed_addons();
// This function does not refer to an addon database, it calls filesystem functions.
// For the sanity of the mp lobby, this list should be fixed for the entire lobby session,
// even if the user changes the contents of the addon directory in the meantime.
DBG_MP << "starting client" << std::endl;
const network::manager net_manager(1,1);
@ -913,12 +921,29 @@ void start_client(game_display& disp, const config& game_config,
switch(type) {
case WESNOTHD_SERVER:
enter_lobby_mode(disp, game_config, state);
bool re_enter;
do {
re_enter = false;
try {
enter_lobby_mode(disp, *game_config_ptr, state, installed_addons);
} catch (lobby_reload_request_exception & ex) {
re_enter = true;
game_config_manager * gcm = game_config_manager::get();
gcm->reload_changed_game_config();
gcm->load_game_config_for_game(state.classification()); // NOTE: Using reload_changed_game_config only doesn't seem to work here
game_config_ptr = &gcm->game_config();
installed_addons = ::installed_addons(); // Refersh the installed add-on list for this session.
gamelist.clear(); //needed to make sure we update which games we have content for
network::send_data(config("refresh_lobby"), 0);
}
} while (re_enter);
break;
case SIMPLE_SERVER:
playmp_controller::set_replay_last_turn(0);
preferences::set_message_private(false);
enter_wait_mode(disp, game_config, state, false);
enter_wait_mode(disp, *game_config_ptr, state, false);
break;
case ABORT_SERVER:
break;

View file

@ -18,6 +18,7 @@
#include "global.hpp"
#include "addon/manager_ui.hpp"
#include "construct_dialog.hpp"
#include "filesystem.hpp"
#include "game_preferences.hpp"
@ -28,6 +29,9 @@
#include "multiplayer_lobby.hpp"
#include "gettext.hpp"
#include "gui/auxiliary/old_markup.hpp"
#include "gui/dialogs/message.hpp" // for gui2::show_message
#include "gui/widgets/window.hpp" // for gui2::twindow::OK
#include "lobby_reload_request_exception.hpp"
#include "log.hpp"
#include "playmp_controller.hpp"
#include "sound.hpp"
@ -43,6 +47,12 @@
static lg::log_domain log_config("config");
#define ERR_CF LOG_STREAM(err, log_config)
static lg::log_domain log_lobby("mp/lobby");
#define ERR_MP LOG_STREAM(err, log_lobby)
#define WRN_MP LOG_STREAM(warn, log_lobby)
#define LOG_MP LOG_STREAM(info, log_lobby)
#define DBG_MP LOG_STREAM(debug, log_lobby)
namespace {
std::vector<std::string> empty_string_vector;
}
@ -172,6 +182,16 @@ void gamebrowser::draw_row(const size_t index, const SDL_Rect& item_rect, ROW_TY
if(!game.have_all_mods && font_color != font::BAD_COLOR) {
font_color = font::DISABLED_COLOR;
}
if(game.addons_outcome != SATISFIED) {
font_color = font::DISABLED_COLOR;
if (game.addons_outcome == NEED_DOWNLOAD) {
no_era_string += _(" (Need to download addons)");
} else {
no_era_string += _(" (Outdated addons)");
}
} /*else {
no_era_string += _(" (You have this addon)");
}*/
const surface status_text(font::get_rendered_text(game.status,
font::SIZE_NORMAL, font_color, TTF_STYLE_BOLD));
@ -435,6 +455,104 @@ void gamebrowser::handle_event(const SDL_Event& event)
}
}
// Determine if this is a campaign or scenario and add info string to this game item.
void gamebrowser::populate_game_item_campaign_or_scenario_info(gamebrowser::game_item & item, const config & game, const config & game_config, bool & verified)
{
if (game["mp_campaign"].empty()) {
if (!game["mp_scenario"].empty()) {
// Check if it's a multiplayer scenario.
const config* level_cfg = &game_config.find_child("multiplayer",
"id", game["mp_scenario"]);
if (!*level_cfg) {
// Check if it's a user map.
level_cfg = &game_config.find_child("generic_multiplayer",
"id", game["mp_scenario"]);
}
if (*level_cfg) {
item.map_info = _("Scenario:");
item.map_info += " ";
item.map_info += (*level_cfg)["name"].str();
// Reloaded games do not match the original scenario hash,
// so it makes no sense to test them,
// they always would appear as remote scenarios.
if (map_hashes_ && !item.reloaded) {
std::string hash = game["hash"];
bool hash_found = false;
BOOST_FOREACH(const config::attribute& i,
map_hashes_.attribute_range()) {
if (i.first == game["mp_scenario"] &&
i.second == hash) {
hash_found = true;
break;
}
}
if (!hash_found) {
item.map_info += "";
item.map_info += _("Remote scenario");
verified = false;
}
}
} else {
utils::string_map symbols;
symbols["scenario_id"] = game["mp_scenario"];
item.map_info =
vgettext("Unknown scenario: $scenario_id", symbols);
verified = false;
}
} else {
item.map_info = _("Unknown scenario");
verified = false;
}
} else { // Is a campaign
const config* level_cfg = &game_config.find_child("campaign", "id",
game["mp_campaign"]);
if (*level_cfg) {
item.map_info = _("Campaign:");
item.map_info += " ";
item.map_info += (*level_cfg)["name"].str();
item.map_info += "";
item.map_info += game["mp_scenario_name"].str();
// Difficulty.
const std::vector<std::string> difficulties =
utils::split((*level_cfg)["difficulties"]);
const std::string difficulty_descriptions =
(*level_cfg)["difficulty_descriptions"];
std::vector<std::string> difficulty_options =
utils::split(difficulty_descriptions, ';');
int index = 0;
//TODO: use difficulties instead of difficulty_descriptions if
//difficulty_descriptions is not available
assert(difficulties.size() == difficulty_options.size());
BOOST_FOREACH(const std::string& difficulty, difficulties) {
if (difficulty == game["difficulty_define"]) {
gui2::tlegacy_menu_item menu_item(difficulty_options[index]);
item.map_info += "";
item.map_info += menu_item.label();
item.map_info += " ";
item.map_info += menu_item.description();
break;
}
index++;
}
} else {
utils::string_map symbols;
symbols["campaign_id"] = game["mp_campaign"];
item.map_info =
vgettext("Unknown campaign: $campaign_id", symbols);
verified = false;
if (game["require_scenario"].to_bool(false)) {
item.have_scenario = false;
}
}
}
}
struct minimap_cache_item {
minimap_cache_item() :
@ -449,14 +567,9 @@ struct minimap_cache_item {
std::string map_info_size;
};
void gamebrowser::set_game_items(const config& cfg, const config& game_config)
// Handle the minimap data. Some caching is taking place to avoid repeatedly rendering the minimaps.
void gamebrowser::populate_game_item_map_info(gamebrowser::game_item & item, const config & game, const config & game_config, bool & verified)
{
const bool scrolled_to_max = (has_scrollbar() && get_position() == get_max_position());
const bool selection_visible = (selected_ >= visible_range_.first && selected_ <= visible_range_.second);
const std::string selected_game = (selected_ < games_.size()) ? games_[selected_].id : "";
item_height_ = 100;
// Don't throw the rendered minimaps away
std::vector<minimap_cache_item> minimap_cache;
for(std::vector<game_item>::iterator oldgame = games_.begin(); oldgame != games_.end(); ++oldgame) {
@ -467,252 +580,272 @@ void gamebrowser::set_game_items(const config& cfg, const config& game_config)
minimap_cache.push_back(item);
}
item.map_data = game["map_data"].str();
if(item.map_data.empty()) {
item.map_data = filesystem::read_map(game["map"]);
}
if(! item.map_data.empty()) {
try {
std::vector<minimap_cache_item>::iterator i;
bool found = false;
for(i = minimap_cache.begin(); i != minimap_cache.end() && !found; ++i) {
if (i->map_data == item.map_data) {
found = true;
item.map_info_size = i->map_info_size;
item.mini_map = i->mini_map;
}
}
if (!found) {
// Parsing the map and generating the minimap are both cpu expensive
gamemap map(boost::make_shared<terrain_type_data>(game_config), item.map_data);
item.mini_map = image::getMinimap(minimap_size_, minimap_size_, map, 0);
item.map_info_size = str_cast(map.w()) + utils::unicode_multiplication_sign
+ str_cast(map.h());
}
} catch (incorrect_map_format_error &e) {
ERR_CF << "illegal map: " << e.message << '\n';
verified = false;
} catch(twml_exception& e) {
ERR_CF << "map could not be loaded: " << e.dev_message << '\n';
verified = false;
}
}
}
// local_item is either an [era] or [modification] tag, something with addon_version and addon_id.
// (These are currently added at add-on loading time in the game_config_manager.)
// It is checked whether the local item's add-on version is required for this game, and if the
// versions are compatible. If it's not, a record is made in the req_list passed as argument.
static mp::ADDON_REQ check_addon_version_compatibility (const config & local_item, const config & game, std::vector<required_addon> & req_list)
{
if (local_item.has_attribute("addon_id") && local_item.has_attribute("addon_version")) {
if (const config & game_req = game.find_child("addon", "id", local_item["addon_id"])) {
// Record object which we will potentially store for this check
required_addon r;
r.addon_id = local_item["addon_id"].str();
const version_info local_ver(local_item["addon_version"].str());
version_info local_min_ver(local_item.has_attribute("addon_min_version") ? local_item["addon_min_version"] : local_item["addon_version"]);
// If UMC didn't specify last compatible version, assume no backwards compatibility.
if (local_min_ver > local_ver) {
// Some sanity checking regarding min version. If the min ver doens't make sense, ignore it.
local_min_ver = local_ver;
}
const version_info remote_ver(game_req["version"].str());
version_info remote_min_ver(game_req.has_attribute("min_version") ? game_req["min_version"] : game_req["version"]);
if (remote_min_ver > remote_ver) {
remote_min_ver = remote_ver;
}
// Check if the host is too out of date to play.
if (local_min_ver > remote_ver) {
r.outcome = CANNOT_SATISFY;
utils::string_map symbols;
symbols["addon"] = r.addon_id; // TODO: Figure out how to ask the add-on manager for the user-friendly name of this add-on.
symbols["host_ver"] = remote_ver.str();
symbols["local_ver"] = local_ver.str();
r.message = vgettext("Host's version of $addon is too old: host's version $host_ver < your version $local_ver.", symbols);
req_list.push_back(r);
return r.outcome;
}
// Check if our version is too out of date to play.
if (remote_min_ver > local_ver) {
r.outcome = NEED_DOWNLOAD;
utils::string_map symbols;
symbols["addon"] = r.addon_id; // TODO: Figure out how to ask the add-on manager for the user-friendly name of this add-on.
symbols["host_ver"] = remote_ver.str();
symbols["local_ver"] = local_ver.str();
r.message = vgettext("Your version of $addon is out of date: host's version $host_ver > your version $local_ver.", symbols);
req_list.push_back(r);
return r.outcome;
}
}
}
return SATISFIED;
}
// Check the era of the game, whether it is available, and compatible with installed content, and add info strings to this game_item.
void gamebrowser::populate_game_item_era_info(gamebrowser::game_item & item, const config & game, const config & game_config, bool & verified) {
if (!game["mp_era"].empty())
{
const config &era_cfg = game_config.find_child("era", "id", game["mp_era"]);
utils::string_map symbols;
symbols["era_id"] = game["mp_era"];
if (era_cfg) {
item.era_and_mod_info = _("Era:");
item.era_and_mod_info += " ";
item.era_and_mod_info += era_cfg["name"].str();
mp::ADDON_REQ result = check_addon_version_compatibility(era_cfg, game, item.addons);
item.addons_outcome = std::max(item.addons_outcome, result); //elevate to most severe error level encountered so far
} else {
if (!game["require_era"].to_bool(true)) {
item.have_era = true;
} else {
item.have_era = false;
}
item.era_and_mod_info = vgettext("Unknown era: $era_id", symbols);
verified = false;
}
} else {
item.era_and_mod_info = _("Unknown era");
verified = false;
}
}
// Check the mods applied to the game, whether they are available / required, and compatible with installed content, and add info strings to this game_item.
void gamebrowser::populate_game_item_mod_info(gamebrowser::game_item & item, const config & game, const config & game_config, bool & verified) {
(void) verified;
if (!game.child_or_empty("modification").empty()) {
item.have_all_mods = true;
item.era_and_mod_info += "";
item.era_and_mod_info += _("Modifications:");
item.era_and_mod_info += " ";
BOOST_FOREACH (const config& m, game.child_range("modification")) {
const config& mod_cfg = game_config.find_child("modification", "id", m["id"]);
if (mod_cfg) {
item.era_and_mod_info += mod_cfg["name"].str();
item.era_and_mod_info += ", ";
mp::ADDON_REQ result = check_addon_version_compatibility(mod_cfg, game, item.addons);
item.addons_outcome = std::max(item.addons_outcome, result); //elevate to most severe error level encountered so far
} else {
item.era_and_mod_info += m["id"].str();
if (m["require_modification"].to_bool(false)) {
item.have_all_mods = false;
item.era_and_mod_info += _(" (missing)");
}
item.era_and_mod_info += ", ";
}
}
item.era_and_mod_info.erase(item.era_and_mod_info.size()-2, 2);
} else {
item.have_all_mods = true;
}
}
// Do this before populating eras and mods, to get reports of missing add-ons in the most sensible order.
void gamebrowser::populate_game_item_addons_installed(gamebrowser::game_item & item, const config & game, const std::vector<std::string> & installed_addons)
{
BOOST_FOREACH(const config & addon, game.child_range("addon")) {
if (addon.has_attribute("id")) {
if (std::find(installed_addons.begin(), installed_addons.end(), addon["id"].str()) == installed_addons.end()) {
required_addon r;
r.addon_id = addon["id"].str();
r.outcome = NEED_DOWNLOAD;
utils::string_map symbols;
symbols["id"] = addon["id"].str();
r.message = vgettext("Missing addon: $id", symbols);
item.addons.push_back(r);
if (item.addons_outcome == SATISFIED) {
item.addons_outcome = NEED_DOWNLOAD;
}
}
}
}
}
// Build an appropriate game_item corresponding to this game (config) which we retrieved from [gamelist] from the server.
void gamebrowser::populate_game_item(gamebrowser::game_item & item, const config & game, const config & game_config, const std::vector<std::string> & installed_addons)
{
bool verified = true;
item.password_required = game["password"].to_bool();
item.reloaded = game["savegame"].to_bool();
item.have_era = true;
item.have_scenario = true;
populate_game_item_addons_installed(item, game, installed_addons);
populate_game_item_campaign_or_scenario_info(item, game, game_config, verified);
populate_game_item_map_info(item, game, game_config, verified);
populate_game_item_era_info(item, game, game_config, verified);
populate_game_item_mod_info(item, game, game_config, verified);
if (item.reloaded) {
item.map_info += "";
item.map_info += _("Reloaded game");
verified = false;
}
item.id = game["id"].str();
item.name = game["name"].str();
std::string turn = game["turn"];
std::string slots = game["slots"];
item.vacant_slots = lexical_cast_default<size_t>(slots, 0);
item.current_turn = 0;
if (!turn.empty()) {
item.started = true;
int index = turn.find_first_of('/');
if (index > -1){
const std::string current_turn = turn.substr(0, index);
item.current_turn = lexical_cast<unsigned int>(current_turn);
}
item.status = _("Turn ") + turn;
} else {
item.started = false;
if (item.vacant_slots > 0) {
item.status = std::string(_n("Vacant Slot:", "Vacant Slots:",
item.vacant_slots)) + " " + slots;
if (item.password_required) {
item.status += std::string(" (") + std::string(_("Password Required")) + ")";
}
}
}
item.use_map_settings = game["mp_use_map_settings"].to_bool();
item.gold = game["mp_village_gold"].str();
if (game["mp_fog"].to_bool()) {
item.vision = _("Fog");
item.fog = true;
if (game["mp_shroud"].to_bool()) {
item.vision += "/";
item.vision += _("Shroud");
item.shroud = true;
} else {
item.shroud = false;
}
} else if (game["mp_shroud"].to_bool()) {
item.vision = _("Shroud");
item.fog = false;
item.shroud = true;
} else {
item.vision = _("none");
item.fog = false;
item.shroud = false;
}
if (game["mp_countdown"].to_bool()) {
item.time_limit = game["mp_countdown_init_time"].str() + " / +"
+ game["mp_countdown_turn_bonus"].str() + " "
+ game["mp_countdown_action_bonus"].str();
} else {
item.time_limit = "";
}
item.xp = game["experience_modifier"].str() + "%";
item.observers = game["observer"].to_bool(true);
item.shuffle_sides = game["shuffle_sides"].to_bool(true);
item.verified = verified;
}
void gamebrowser::set_game_items(const config& cfg, const config& game_config, const std::vector<std::string> & installed_addons)
{
//DBG_MP << "** gamelist **\n" << cfg.debug() << "****\n";
const bool scrolled_to_max = (has_scrollbar() && get_position() == get_max_position());
const bool selection_visible = (selected_ >= visible_range_.first && selected_ <= visible_range_.second);
const std::string selected_game = (selected_ < games_.size()) ? games_[selected_].id : "";
item_height_ = 100;
games_.clear();
BOOST_FOREACH(const config &game, cfg.child("gamelist").child_range("game"))
{
bool verified = true;
games_.push_back(game_item());
games_.back().password_required = game["password"].to_bool();
games_.back().reloaded = game["savegame"].to_bool();
games_.back().have_era = true;
games_.back().have_scenario = true;
if (game["mp_campaign"].empty()) {
if (!game["mp_scenario"].empty()) {
// Check if it's a multiplayer scenario.
const config* level_cfg = &game_config.find_child("multiplayer",
"id", game["mp_scenario"]);
if (!*level_cfg) {
// Check if it's a user map.
level_cfg = &game_config.find_child("generic_multiplayer",
"id", game["mp_scenario"]);
}
if (*level_cfg) {
games_.back().map_info = _("Scenario:");
games_.back().map_info += " ";
games_.back().map_info += (*level_cfg)["name"].str();
// Reloaded games do not match the original scenario hash,
// so it makes no sense to test them,
// they always would appear as remote scenarios.
if (map_hashes_ && !games_.back().reloaded) {
std::string hash = game["hash"];
bool hash_found = false;
BOOST_FOREACH(const config::attribute& i,
map_hashes_.attribute_range()) {
if (i.first == game["mp_scenario"] &&
i.second == hash) {
hash_found = true;
break;
}
}
if (!hash_found) {
games_.back().map_info += "";
games_.back().map_info += _("Remote scenario");
verified = false;
}
}
} else {
utils::string_map symbols;
symbols["scenario_id"] = game["mp_scenario"];
games_.back().map_info =
vgettext("Unknown scenario: $scenario_id", symbols);
verified = false;
}
} else {
games_.back().map_info = _("Unknown scenario");
verified = false;
}
} else { // Is a campaign
const config* level_cfg = &game_config.find_child("campaign", "id",
game["mp_campaign"]);
if (*level_cfg) {
games_.back().map_info = _("Campaign:");
games_.back().map_info += " ";
games_.back().map_info += (*level_cfg)["name"].str();
games_.back().map_info += "";
games_.back().map_info += game["mp_scenario_name"].str();
// Difficulty.
const std::vector<std::string> difficulties =
utils::split((*level_cfg)["difficulties"]);
const std::string difficulty_descriptions =
(*level_cfg)["difficulty_descriptions"];
std::vector<std::string> difficulty_options =
utils::split(difficulty_descriptions, ';');
int index = 0;
//TODO: use difficulties instead of difficulty_descriptions if
//difficulty_descriptions is not available
assert(difficulties.size() == difficulty_options.size());
BOOST_FOREACH(const std::string& difficulty, difficulties) {
if (difficulty == game["difficulty_define"]) {
gui2::tlegacy_menu_item menu_item(difficulty_options[index]);
games_.back().map_info += "";
games_.back().map_info += menu_item.label();
games_.back().map_info += " ";
games_.back().map_info += menu_item.description();
break;
}
index++;
}
} else {
utils::string_map symbols;
symbols["campaign_id"] = game["mp_campaign"];
games_.back().map_info =
vgettext("Unknown campaign: $campaign_id", symbols);
verified = false;
if (game["require_scenario"].to_bool(false)) {
games_.back().have_scenario = false;
}
}
}
games_.back().map_data = game["map_data"].str();
if(games_.back().map_data.empty()) {
games_.back().map_data = filesystem::read_map(game["map"]);
}
if(! games_.back().map_data.empty()) {
try {
std::vector<minimap_cache_item>::iterator i;
bool found = false;
for(i = minimap_cache.begin(); i != minimap_cache.end() && !found; ++i) {
if (i->map_data == games_.back().map_data) {
found = true;
games_.back().map_info_size = i->map_info_size;
games_.back().mini_map = i->mini_map;
}
}
if (!found) {
// Parsing the map and generating the minimap are both cpu expensive
gamemap map(boost::make_shared<terrain_type_data>(game_config), games_.back().map_data);
games_.back().mini_map = image::getMinimap(minimap_size_, minimap_size_, map, 0);
games_.back().map_info_size = str_cast(map.w()) + utils::unicode_multiplication_sign
+ str_cast(map.h());
}
} catch (incorrect_map_format_error &e) {
ERR_CF << "illegal map: " << e.message << '\n';
verified = false;
} catch(twml_exception& e) {
ERR_CF << "map could not be loaded: " << e.dev_message << '\n';
verified = false;
}
}
if (!game["mp_era"].empty())
{
const config &era_cfg = game_config.find_child("era", "id", game["mp_era"]);
utils::string_map symbols;
symbols["era_id"] = game["mp_era"];
if (era_cfg) {
games_.back().era_and_mod_info = _("Era:");
games_.back().era_and_mod_info += " ";
games_.back().era_and_mod_info += era_cfg["name"].str();
} else {
if (!game["require_era"].to_bool(true)) {
games_.back().have_era = true;
} else {
games_.back().have_era = false;
}
games_.back().era_and_mod_info = vgettext("Unknown era: $era_id", symbols);
verified = false;
}
} else {
games_.back().era_and_mod_info = _("Unknown era");
verified = false;
}
if (!game.child_or_empty("modification").empty()) {
games_.back().have_all_mods = true;
games_.back().era_and_mod_info += "";
games_.back().era_and_mod_info += _("Modifications:");
games_.back().era_and_mod_info += " ";
BOOST_FOREACH (const config& m, game.child_range("modification")) {
const config& mod_cfg = game_config.find_child("modification", "id", m["id"]);
if (mod_cfg) {
games_.back().era_and_mod_info += mod_cfg["name"].str();
games_.back().era_and_mod_info += ", ";
} else {
games_.back().era_and_mod_info += m["id"].str();
if (m["require_modification"].to_bool(false)) {
games_.back().have_all_mods = false;
games_.back().era_and_mod_info += _(" (missing)");
}
games_.back().era_and_mod_info += ", ";
}
}
games_.back().era_and_mod_info.erase(games_.back().era_and_mod_info.size()-2, 2);
} else {
games_.back().have_all_mods = true;
}
if (games_.back().reloaded) {
games_.back().map_info += "";
games_.back().map_info += _("Reloaded game");
verified = false;
}
games_.back().id = game["id"].str();
games_.back().name = game["name"].str();
std::string turn = game["turn"];
std::string slots = game["slots"];
games_.back().vacant_slots = lexical_cast_default<size_t>(slots, 0);
games_.back().current_turn = 0;
if (!turn.empty()) {
games_.back().started = true;
int index = turn.find_first_of('/');
if (index > -1){
const std::string current_turn = turn.substr(0, index);
games_.back().current_turn = lexical_cast<unsigned int>(current_turn);
}
games_.back().status = _("Turn ") + turn;
} else {
games_.back().started = false;
if (games_.back().vacant_slots > 0) {
games_.back().status = std::string(_n("Vacant Slot:", "Vacant Slots:",
games_.back().vacant_slots)) + " " + slots;
if (games_.back().password_required) {
games_.back().status += std::string(" (") + std::string(_("Password Required")) + ")";
}
}
}
games_.back().use_map_settings = game["mp_use_map_settings"].to_bool();
games_.back().gold = game["mp_village_gold"].str();
if (game["mp_fog"].to_bool()) {
games_.back().vision = _("Fog");
games_.back().fog = true;
if (game["mp_shroud"].to_bool()) {
games_.back().vision += "/";
games_.back().vision += _("Shroud");
games_.back().shroud = true;
} else {
games_.back().shroud = false;
}
} else if (game["mp_shroud"].to_bool()) {
games_.back().vision = _("Shroud");
games_.back().fog = false;
games_.back().shroud = true;
} else {
games_.back().vision = _("none");
games_.back().fog = false;
games_.back().shroud = false;
}
if (game["mp_countdown"].to_bool()) {
games_.back().time_limit = game["mp_countdown_init_time"].str() + " / +"
+ game["mp_countdown_turn_bonus"].str() + " "
+ game["mp_countdown_action_bonus"].str();
} else {
games_.back().time_limit = "";
}
games_.back().xp = game["experience_modifier"].str() + "%";
games_.back().observers = game["observer"].to_bool(true);
games_.back().shuffle_sides = game["shuffle_sides"].to_bool(true);
games_.back().verified = verified;
populate_game_item(games_.back(), game, game_config, installed_addons);
// Hack...
if(preferences::fi_invert() ? game_matches_filter(games_.back(), cfg) : !game_matches_filter(games_.back(), cfg)) games_.pop_back();
}
@ -857,7 +990,7 @@ bool lobby::lobby_sorter::less(int column, const gui::menu::item& row1, const gu
return basic_sorter::less(column,row1,row2);
}
lobby::lobby(game_display& disp, const config& cfg, chat& c, config& gamelist) :
lobby::lobby(game_display& disp, const config& cfg, chat& c, config& gamelist, const std::vector<std::string> & installed_addons) :
mp::ui(disp, _("Game Lobby"), cfg, c, gamelist),
game_vacant_slots_(),
@ -878,7 +1011,8 @@ lobby::lobby(game_display& disp, const config& cfg, chat& c, config& gamelist) :
last_selected_game_(-1), sorter_(gamelist),
games_menu_(disp.video(),cfg.child("multiplayer_hashes")),
minimaps_(),
search_string_(preferences::fi_text())
search_string_(preferences::fi_text()),
installed_addons_(installed_addons)
{
std::vector<std::string> replay_options_strings_;
replay_options_strings_.push_back(_("Normal Replays"));
@ -1004,7 +1138,7 @@ void lobby::gamelist_updated(bool silent)
// No gamelist yet. Do not update anything.
return;
}
games_menu_.set_game_items(gamelist(), game_config());
games_menu_.set_game_items(gamelist(), game_config(), installed_addons_);
join_game_.enable(games_menu_.selection_is_joinable());
observe_game_.enable(games_menu_.selection_is_observable());
}
@ -1020,14 +1154,72 @@ void lobby::process_event()
process_event_impl(data);
}
static void handle_addon_requirements_gui(display & disp, const std::vector<required_addon> & reqs, mp::ADDON_REQ addon_outcome)
{
if (addon_outcome == CANNOT_SATISFY) {
std::string e_title = _("Incompatible user-made content.");
std::string err_msg = _("This game cannot be joined because the host has out-of-date add-ons which are incompatible with your version. You might suggest to them that they update their add-ons.");
err_msg +="\n\n";
err_msg += _("Details:");
err_msg += "\n";
BOOST_FOREACH(const required_addon & a, reqs) {
if (a.outcome == CANNOT_SATISFY) {
err_msg += a.message;
err_msg += "\n";
}
}
gui2::show_message(disp.video(), e_title, err_msg, gui2::tmessage::auto_close);
} else if (addon_outcome == NEED_DOWNLOAD) {
std::string e_title = _("Missing user-made content.");
std::string err_msg = _("This game requires one or more user-made addons to be installed or updated in order to join. Do you want to try to install them?");
err_msg +="\n\n";
err_msg += _("Details:");
err_msg += "\n";
std::vector<std::string> needs_download;
BOOST_FOREACH(const required_addon & a, reqs) {
if (a.outcome == NEED_DOWNLOAD) {
err_msg += a.message;
err_msg += "\n";
needs_download.push_back(a.addon_id);
} else if (a.outcome == CANNOT_SATISFY) {
assert(false);
}
}
assert(needs_download.size() > 0);
if (gui2::show_message(disp.video(), e_title, err_msg, gui2::tmessage::yes_no_buttons) == gui2::twindow::OK) {
ad_hoc_addon_fetch_session(disp, needs_download);
throw lobby_reload_request_exception();
}
}
}
// The return value should be true if the gui result was not chnaged
void lobby::process_event_impl(const process_event_data & data)
{
join_game_.enable(games_menu_.selection_is_joinable());
observe_game_.enable(games_menu_.selection_is_observable());
// check whehter to try to download addons
if ((games_menu_.selected() || data.observe || data.join) && games_menu_.selection_needs_addons())
{
if (const std::vector<required_addon> * reqs = games_menu_.selection_addon_requirements()) {
handle_addon_requirements_gui(disp(), *reqs, games_menu_.selection_addon_outcome());
} else {
gui2::show_error_message(video(), _("Something is wrong with the addon version check database supporting the multiplayer lobby, please report this at bugs.wesnoth.org."));
}
games_menu_.reset_selection();
return ;
}
const bool observe = (data.observe || (games_menu_.selected() && !games_menu_.selection_is_joinable())) && games_menu_.selection_is_observable();
const bool join = (data.join || games_menu_.selected()) && games_menu_.selection_is_joinable();
games_menu_.reset_selection();
preferences::set_skip_mp_replay(replay_options_.selected() == 1);
preferences::set_blindfold_replay(replay_options_.selected() == 2);

View file

@ -20,6 +20,7 @@
#include "multiplayer_ui.hpp"
#include "game_preferences.hpp"
#include "image.hpp"
#include "serialization/string_utils.hpp"
class config;
class video;
@ -32,6 +33,15 @@ class game_display;
* games.
*/
namespace mp {
enum ADDON_REQ { SATISFIED, NEED_DOWNLOAD, CANNOT_SATISFY };
struct required_addon {
std::string addon_id;
ADDON_REQ outcome;
std::string message;
};
class gamebrowser : public gui::menu {
public:
struct game_item {
@ -62,7 +72,9 @@ public:
password_required(false),
have_scenario(false),
have_era(false),
have_all_mods(false)
have_all_mods(false),
addons(),
addons_outcome(SATISFIED)
{
}
@ -92,31 +104,48 @@ public:
bool have_scenario;
bool have_era;
bool have_all_mods;
std::vector<required_addon> addons;
ADDON_REQ addons_outcome;
};
gamebrowser(CVideo& video, const config &map_hashes);
void scroll(unsigned int pos);
void handle_event(const SDL_Event& event);
void set_inner_location(const SDL_Rect& rect);
void set_item_height(unsigned int height);
void set_game_items(const config& cfg, const config& game_config);
void populate_game_item(game_item &, const config &, const config &, const std::vector<std::string> &);
void populate_game_item_campaign_or_scenario_info(game_item &, const config &, const config &, bool &);
void populate_game_item_map_info(game_item &, const config &, const config &, bool &);
void populate_game_item_era_info(game_item &, const config &, const config &, bool &);
void populate_game_item_mod_info(game_item &, const config &, const config &, bool &);
void populate_game_item_addons_installed(game_item &, const config &, const std::vector<std::string> &);
void set_game_items(const config& cfg, const config& game_config, const std::vector<std::string> & installed_addons);
void draw();
void draw_contents();
void draw_row(const size_t row_index, const SDL_Rect& rect, ROW_TYPE type);
SDL_Rect get_item_rect(size_t index) const;
bool empty() const { return games_.empty(); }
bool selection_is_joinable() const
{ return empty() ? false : (games_[selected_].vacant_slots > 0 &&
!games_[selected_].started &&
games_[selected_].have_scenario &&
games_[selected_].have_era &&
games_[selected_].have_all_mods); }
// Moderators may observe any game.
{ return selection_in_joinable_state() && (!selection_needs_content() || selection_needs_addons()); }
bool selection_is_observable() const
{ return empty() ? false : (games_[selected_].observers &&
games_[selected_].have_scenario &&
games_[selected_].have_era &&
games_[selected_].have_all_mods) ||
preferences::is_authenticated(); }
{ return selection_in_observable_state() && (!selection_needs_content() || selection_needs_addons()); }
bool selection_needs_content() const
{ return empty() ? false : !games_[selected_].have_era ||
!games_[selected_].have_all_mods ||
!games_[selected_].have_scenario; }
bool selection_needs_addons() const
{ return empty() ? false : (games_[selected_].addons_outcome != SATISFIED); }
bool selection_in_joinable_state() const
{ return empty() ? false : (games_[selected_].vacant_slots > 0 && !games_[selected_].started); }
// Moderators may observe any game.
bool selection_in_observable_state() const
{ return empty() ? false : (games_[selected_].observers || preferences::is_authenticated()); }
ADDON_REQ selection_addon_outcome() const
{ return empty() ? SATISFIED : games_[selected_].addons_outcome; }
const std::vector<required_addon> * selection_addon_requirements() const
{ return empty() ? NULL : &games_[selected_].addons; }
bool selected() const { return double_clicked_ && !empty(); }
void reset_selection() { double_clicked_ = false; }
int selection() const { return selected_; }
@ -156,7 +185,7 @@ private:
class lobby : public ui
{
public:
lobby(game_display& d, const config& cfg, chat& c, config& gamelist);
lobby(game_display& d, const config& cfg, chat& c, config& gamelist, const std::vector<std::string> & installed_addons);
virtual void process_event();
@ -206,6 +235,8 @@ private:
std::string search_string_;
const std::vector<std::string> & installed_addons_;
struct process_event_data {
bool join;
bool observe;

View file

@ -21,6 +21,8 @@
#include "mp_game_settings.hpp"
#include "formula_string_utils.hpp"
#include <boost/foreach.hpp>
mp_game_settings::mp_game_settings() :
savegame_config(),
name(),
@ -52,7 +54,8 @@ mp_game_settings::mp_game_settings() :
shuffle_sides(false),
saved_game(false),
random_faction_mode(DEFAULT),
options()
options(),
addons()
{}
mp_game_settings::mp_game_settings(const config& cfg)
@ -87,7 +90,11 @@ mp_game_settings::mp_game_settings(const config& cfg)
, saved_game(cfg["savegame"].to_bool())
, random_faction_mode(string_to_RANDOM_FACTION_MODE_default(cfg["random_faction_mode"].str(), DEFAULT))
, options(cfg.child_or_empty("options"))
, addons()
{
BOOST_FOREACH(const config & a, cfg.child_range("addon")) {
addons.push_back(addon_version_info(a));
}
}
config mp_game_settings::to_config() const
@ -124,5 +131,32 @@ config mp_game_settings::to_config() const
cfg["savegame"] = saved_game;
cfg.add_child("options", options);
BOOST_FOREACH(const addon_version_info & a, addons) {
a.write(cfg.add_child("addon"));
}
return cfg;
}
mp_game_settings::addon_version_info::addon_version_info(const config & cfg)
: id(cfg["id"])
, version()
, min_version()
{
if (cfg.has_attribute("version") && !cfg["version"].empty()) {
version = cfg["version"].str();
}
if (cfg.has_attribute("min_version") && !cfg["min_version"].empty()) {
min_version = cfg["min_version"].str();
}
}
void mp_game_settings::addon_version_info::write(config & cfg) const {
cfg["id"] = id;
if (version) {
cfg["version"] = *version;
}
if (min_version) {
cfg["min_version"] = *min_version;
}
}

View file

@ -21,6 +21,9 @@
#include "gettext.hpp"
#include "make_enum.hpp"
#include "savegame_config.hpp"
#include "version.hpp"
#include <boost/optional.hpp>
struct mp_game_settings : public savegame::savegame_config
{
@ -72,6 +75,17 @@ struct mp_game_settings : public savegame::savegame_config
RANDOM_FACTION_MODE random_faction_mode;
config options;
struct addon_version_info {
std::string id;
boost::optional<version_info> version;
boost::optional<version_info> min_version;
explicit addon_version_info(const config &);
void write(config &) const;
};
std::vector<addon_version_info> addons;
};
MAKE_ENUM_STREAM_OPS2(mp_game_settings, RANDOM_FACTION_MODE)

View file

@ -36,6 +36,7 @@
#include "saved_game.hpp"
#include "carryover.hpp"
#include "config_assign.hpp"
#include "cursor.hpp"
#include "log.hpp"
#include "game_config_manager.hpp"
@ -221,6 +222,8 @@ private:
std::string type_;
};
// Gets the ids of the mp_era and modifications which were set to be active, then fetches these configs from the game_config and copies their [event] and [lua] to the starting_pos_.
// At this time, also collect the addon_id attributes which appeared in them and put this list in the addon_ids attribute of the mp_settings.
void saved_game::expand_mp_events()
{
expand_scenario();
@ -239,10 +242,45 @@ void saved_game::expand_mp_events()
if(const config& cfg = game_config_manager::get()->
game_config().find_child(mod.type, "id", mod.id))
{
// Note the addon_id if this mod is required to play the game in mp
std::string require_attr = "require_" + mod.type;
if (cfg.has_attribute("addon_id") && !cfg["addon_id"].empty() && cfg[require_attr].to_bool(true)) {
config addon_data = config_of("id",cfg["addon_id"])("version", cfg["addon_version"])("min_version", cfg["addon_min_version"]);
mp_game_settings::addon_version_info new_data(addon_data);
bool found = false;
BOOST_FOREACH(mp_game_settings::addon_version_info & addon, mp_settings_.addons) {
if (addon.id == cfg["addon_id"].str()) {
if (found) {
ERR_NG << "An mp_settings addons list contains repeated entries, this indicates a bug which may affect add-on versioning. Please report at bugs.wesnoth.org. Relevant add-on: '" << cfg["addon_id"].str() <<"'\n";
}
found = true;
if (new_data.version) {
if (!addon.version || (*addon.version != *new_data.version)) {
WRN_NG << "Addon version data mismatch -- not all local WML has same version of '" << cfg["addon_id"].str() << "' addon.\n";
}
}
if (addon.version && !new_data.version) {
WRN_NG << "Addon version data mismatch -- not all local WML has same version of '" << cfg["addon_id"].str() << "' addon.\n";
}
if (new_data.min_version) {
if (!addon.min_version || (*new_data.min_version > *addon.min_version)) {
addon.min_version = *new_data.min_version;
}
}
}
}
if (!found) {
mp_settings_.addons.push_back(new_data);
}
}
// Copy events
BOOST_FOREACH(const config& modevent, cfg.child_range("event"))
{
this->starting_pos_.add_child("event", modevent);
}
// Copy lua
BOOST_FOREACH(const config& modlua, cfg.child_range("lua"))
{
this->starting_pos_.add_child("lua", modlua);

View file

@ -455,6 +455,14 @@ BOOST_AUTO_TEST_CASE(test_gui2)
std::remove(list.begin(), list.end(), "lua_interpreter")
, list.end());
//Window 'addon_description' registered but not tested.
//Window 'addon_filter_options' registered but not tested.
//Window 'addon_uninstall_list' registered but not tested.
//Window 'network_transmission' registered but not tested.
list.erase(std::remove(list.begin(), list.end(), "addon_description"), list.end());
list.erase(std::remove(list.begin(), list.end(), "addon_filter_options"), list.end());
list.erase(std::remove(list.begin(), list.end(), "addon_uninstall_list"), list.end());
list.erase(std::remove(list.begin(), list.end(), "network_transmission"), list.end());
// Test size() instead of empty() to get the number of offenders
BOOST_CHECK_EQUAL(list.size(), 0);