The Battle for Wesnoth  1.13.10+dev
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
lobby_data.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2017 by Tomasz Sniatowski <kailoran@gmail.com>
3  Part of the Battle for Wesnoth Project http://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
16 
17 #include "config.hpp"
19 #include "preferences/game.hpp"
21 #include "filesystem.hpp"
22 #include "formatter.hpp"
23 #include "formula/string_utils.hpp"
24 #include "gettext.hpp"
25 #include "lexical_cast.hpp"
26 #include "log.hpp"
27 #include "map/map.hpp"
28 #include "map/exception.hpp"
29 #include "terrain/type_data.hpp"
30 #include "utils/general.hpp"
31 #include "wml_exception.hpp"
32 #include "version.hpp"
33 
34 #include <iterator>
35 
36 #include <boost/algorithm/string.hpp>
37 
38 static lg::log_domain log_config("config");
39 #define ERR_CF LOG_STREAM(err, log_config)
40 static lg::log_domain log_engine("engine");
41 #define WRN_NG LOG_STREAM(warn, log_engine)
42 
43 static lg::log_domain log_lobby("lobby");
44 #define DBG_LB LOG_STREAM(info, log_lobby)
45 #define LOG_LB LOG_STREAM(info, log_lobby)
46 #define ERR_LB LOG_STREAM(err, log_lobby)
47 
48 namespace mp {
49 
51  const std::string& user,
52  const std::string& message)
53  : timestamp(timestamp), user(user), message(message)
54 {
55 }
56 
58 {
59 }
60 
62  const std::string& user,
63  const std::string& message)
64 {
65  history_.emplace_back(timestamp, user, message);
66 }
67 
68 
69 void chat_session::add_message(const std::string& user, const std::string& message)
70 {
71  add_message(time(nullptr), user, message);
72 }
73 
75 {
76  history_.clear();
77 }
78 
79 room_info::room_info(const std::string& name) : name_(name), members_(), log_()
80 {
81 }
82 
83 bool room_info::is_member(const std::string& user) const
84 {
85  return members_.find(user) != members_.end();
86 }
87 
89 {
90  members_.insert(user);
91 }
92 
94 {
95  members_.erase(user);
96 }
97 
99 {
100  members_.clear();
101  for(const auto & m : data.child_range("member"))
102  {
103  members_.insert(m["name"]);
104  }
105 }
106 
108  : name(c["name"])
109  , game_id(c["game_id"])
110  , relation(ME)
111  , state(game_id == 0 ? LOBBY : GAME)
112  , registered(c["registered"].to_bool())
113  , observing(c["status"] == "observing")
114 {
115  update_relation();
116 }
117 
118 void user_info::update_state(int selected_game_id,
119  const room_info* current_room /*= nullptr*/)
120 {
121  if(game_id != 0) {
122  if(game_id == selected_game_id) {
123  state = SEL_GAME;
124  } else {
125  state = GAME;
126  }
127  } else {
128  if(current_room != nullptr && current_room->is_member(name)) {
129  state = SEL_ROOM;
130  } else {
131  state = LOBBY;
132  }
133  }
134  update_relation();
135 }
136 
138 {
139  if(name == preferences::login()) {
140  relation = ME;
141  } else if(preferences::is_ignored(name)) {
142  relation = IGNORED;
143  } else if(preferences::is_friend(name)) {
144  relation = FRIEND;
145  } else {
146  relation = NEUTRAL;
147  }
148 }
149 
150 // Returns an abbreviated form of the provided string - ie, 'Ageless Era' should become 'AE'
151 static std::string make_short_name(const std::string& long_name)
152 {
153  if(long_name.empty()) {
154  return "";
155  }
156 
157  size_t pos = 0;
158 
159  std::stringstream ss;
160  ss << long_name[pos];
161 
162  while(pos < long_name.size()) {
163  pos = long_name.find(' ', pos + 1);
164 
165  if(pos <= long_name.size() - 2) {
166  ss << long_name[pos + 1];
167  }
168  }
169 
170  return ss.str();
171 }
172 
173 game_info::game_info(const config& game, const config& game_config, const std::vector<std::string>& installed_addons)
174  : id(game["id"])
175  , map_data(game["map_data"])
176  , name(game["name"])
177  , scenario()
178  , remote_scenario(false)
179  , map_info()
180  , map_size_info()
181  , era()
182  , era_short()
183  , gold(game["mp_village_gold"])
184  , support(game["mp_village_support"])
185  , xp(game["experience_modifier"].str() + "%")
186  , vision()
187  , status()
188  , time_limit()
189  , vacant_slots(lexical_cast_default<int>(game["slots"], 0)) // Can't use to_int() here.
190  , current_turn(0)
191  , reloaded(game["savegame"].to_bool())
192  , started(false)
193  , fog(game["mp_fog"].to_bool())
194  , shroud(game["mp_shroud"].to_bool())
195  , observers(game["observer"].to_bool(true))
196  , shuffle_sides(game["shuffle_sides"].to_bool(true))
197  , use_map_settings(game["mp_use_map_settings"].to_bool())
198  , registered_users_only(game["registered_users_only"].to_bool())
199  , verified(true)
200  , password_required(game["password"].to_bool())
201  , have_era(true)
202  , have_all_mods(true)
203  , has_friends(false)
204  , has_ignored(false)
205  , display_status(NEW)
206  , required_addons()
207  , addons_outcome(SATISFIED)
208 {
209  const auto parse_requirements = [&](const config& c, const std::string& id_key) {
210  if(c.has_attribute(id_key)) {
211  if(std::find(installed_addons.begin(), installed_addons.end(), c[id_key].str()) == installed_addons.end()) {
212  required_addon r;
213  r.addon_id = c[id_key].str();
215 
216  r.message = vgettext("Missing addon: $id", {{"id", c[id_key].str()}});
217  required_addons.push_back(r);
218  if(addons_outcome == SATISFIED) {
220  }
221  }
222  }
223  };
224 
225  for(const config& addon : game.child_range("addon")) {
226  parse_requirements(addon, "id");
227  }
228 
229  /*
230  * Modifications have a different format than addons. The id and addon_id are keys sent by the
231  * server, so we have to parse them separately here and add them to the required_addons vector.
232  */
233  for(const config& mod : game.child_range("modification")) {
234  parse_requirements(mod, "addon_id");
235  }
236 
237  std::string turn = game["turn"];
238  if(!game["mp_era"].empty()) {
239  const config& era_cfg = game_config.find_child("era", "id", game["mp_era"]);
240  if(era_cfg) {
241  era = era_cfg["name"].str();
242  era_short = era_cfg["short_name"].str();
243  if(era_short.empty()) {
245  }
246 
247  ADDON_REQ result = check_addon_version_compatibility(era_cfg, game);
248  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
249  } else {
250  have_era = !game["require_era"].to_bool(true);
251  era = vgettext("Unknown era: $era_id", {{"era_id", game["mp_era_addon_id"].str()}});
253  verified = false;
254 
256  }
257  } else {
258  era = _("Unknown era");
259  era_short = "??";
260  verified = false;
261  }
262 
263  std::stringstream info_stream;
264  info_stream << era;
265 
266  if(!game.child_or_empty("modification").empty()) {
267  for(const config& cfg : game.child_range("modification")) {
268  if(const config& mod = game_config.find_child("modification", "id", cfg["id"])) {
269  mod_info += (mod_info.empty() ? "" : ", ") + mod["name"].str();
270 
271  if(cfg["require_modification"].to_bool(false)) {
272  ADDON_REQ result = check_addon_version_compatibility(mod, game);
273  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
274  }
275  } else {
276  mod_info += (mod_info.empty() ? "" : ", ") + cfg["id"].str();
277 
278  if(cfg["require_modification"].to_bool(false)) {
279  have_all_mods = false;
280  mod_info += " " + _("(missing)");
281 
283  }
284  }
285  }
286  }
287 
288  if(map_data.empty()) {
289  map_data = filesystem::read_map(game["mp_scenario"]);
290  }
291 
292  if(map_data.empty()) {
293  info_stream << " — ??×??";
294  } else {
295  try {
296  gamemap map(std::make_shared<terrain_type_data>(game_config), map_data);
297  std::ostringstream msi;
298  msi << map.w() << font::unicode_multiplication_sign << map.h();
299  map_size_info = msi.str();
300  info_stream << " — " + map_size_info;
301  } catch(incorrect_map_format_error& e) {
302  ERR_CF << "illegal map: " << e.message << std::endl;
303  verified = false;
304  } catch(wml_exception& e) {
305  ERR_CF << "map could not be loaded: " << e.dev_message << '\n';
306  verified = false;
307  }
308  }
309  info_stream << " ";
310 
311  //
312  // Check scenarios and campaigns
313  //
314  if(!game["mp_scenario"].empty() && game["mp_campaign"].empty()) {
315  // Check if it's a multiplayer scenario
316  const config* level_cfg = &game_config.find_child("multiplayer", "id", game["mp_scenario"]);
317 
318  // Check if it's a user map
319  if(!*level_cfg) {
320  level_cfg = &game_config.find_child("generic_multiplayer", "id", game["mp_scenario"]);
321  }
322 
323  if(*level_cfg) {
324  scenario = formatter() << "<b>" << _("(S)") << "</b>" << " " << (*level_cfg)["name"].str();
325  info_stream << scenario;
326 
327  // Reloaded games do not match the original scenario hash, so it makes no sense
328  // to test them, since they always would appear as remote scenarios
329  if(!reloaded) {
330  if(const config& hashes = game_config.child("multiplayer_hashes")) {
331  std::string hash = game["hash"];
332  bool hash_found = false;
333  for(const auto & i : hashes.attribute_range()) {
334  if(i.first == game["mp_scenario"] && i.second == hash) {
335  hash_found = true;
336  break;
337  }
338  }
339 
340  if(!hash_found) {
341  remote_scenario = true;
342  info_stream << " — ";
343  info_stream << _("Remote scenario");
344  verified = false;
345  }
346  }
347  }
348 
349  if((*level_cfg)["require_scenario"].to_bool(false)) {
350  ADDON_REQ result = check_addon_version_compatibility((*level_cfg), game);
351  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
352  }
353  } else {
354  scenario = vgettext("Unknown scenario: $scenario_id", {{"scenario_id", game["mp_scenario_name"].str()}});
355  info_stream << scenario;
356  verified = false;
357  }
358  } else if(!game["mp_campaign"].empty()) {
359  if(const config& level_cfg = game_config.find_child("campaign", "id", game["mp_campaign"])) {
360  std::stringstream campaign_text;
361  campaign_text
362  << "<b>" << _("(C)") << "</b>" << " "
363  << level_cfg["name"] << " — "
364  << game["mp_scenario_name"];
365 
366  // Difficulty
367  config difficulties = gui2::dialogs::generate_difficulty_config(level_cfg);
368  for(const config& difficulty : difficulties.child_range("difficulty")) {
369  if(difficulty["define"] == game["difficulty_define"]) {
370  campaign_text << " — " << difficulty["description"];
371 
372  break;
373  }
374  }
375 
376  scenario = campaign_text.str();
377  info_stream << campaign_text.rdbuf();
378 
379  // TODO: should we have this?
380  //if(game["require_scenario"].to_bool(false)) {
381  ADDON_REQ result = check_addon_version_compatibility(level_cfg, game);
382  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
383  //}
384  } else {
385  scenario = vgettext("Unknown campaign: $campaign_id", {{"campaign_id", game["mp_campaign"].str()}});
386  info_stream << scenario;
387  verified = false;
388  }
389  } else {
390  scenario = _("Unknown scenario");
391  info_stream << scenario;
392  verified = false;
393  }
394 
395  // Remove any newlines that might have been in game titles
396  boost::replace_all(scenario, "\n", " " + font::unicode_em_dash + " ");
397 
398  if(reloaded) {
399  info_stream << " — ";
400  info_stream << _("Reloaded game");
401  verified = false;
402  }
403 
404  if(!turn.empty()) {
405  started = true;
406  int index = turn.find_first_of('/');
407  if(index > -1) {
408  const std::string current_turn_string = turn.substr(0, index);
409  current_turn = lexical_cast<unsigned int>(current_turn_string);
410  }
411  status = _("Turn") + " " + turn;
412  } else {
413  started = false;
414  if(vacant_slots > 0) {
415  status = _n("Vacant Slot:", "Vacant Slots:", vacant_slots) + " " + game["slots"];
416  }
417  }
418 
419  if(fog) {
420  vision = _("Fog");
421  if(shroud) {
422  vision += "/";
423  vision += _("Shroud");
424  }
425  } else if(shroud) {
426  vision = _("Shroud");
427  } else {
428  vision = _("none");
429  }
430 
431  if(game["mp_countdown"].to_bool()) {
433  << game["mp_countdown_init_time"].str() << "+"
434  << game["mp_countdown_turn_bonus"].str() << "/"
435  << game["mp_countdown_action_bonus"].str();
436  }
437 
438  map_info = info_stream.str();
439 }
440 
442 {
443  if(!local_item.has_attribute("addon_id") || !local_item.has_attribute("addon_version")) {
444  return SATISFIED;
445  }
446 
447  if(const config& game_req = game.find_child("addon", "id", local_item["addon_id"])) {
448  required_addon r {local_item["addon_id"].str(), SATISFIED, ""};
449 
450  // Local version
451  const version_info local_ver(local_item["addon_version"].str());
452  version_info local_min_ver(local_item.has_attribute("addon_min_version") ? local_item["addon_min_version"] : local_item["addon_version"]);
453 
454  // If the UMC didn't specify last compatible version, assume no backwards compatibility.
455  // Also apply some sanity checking regarding min version; if the min ver doens't make sense, ignore it.
456  local_min_ver = std::min(local_min_ver, local_ver);
457 
458  // Remote version
459  const version_info remote_ver(game_req["version"].str());
460  version_info remote_min_ver(game_req.has_attribute("min_version") ? game_req["min_version"] : game_req["version"]);
461 
462  remote_min_ver = std::min(remote_min_ver, remote_ver);
463 
464  // Check if the host is too out of date to play.
465  if(local_min_ver > remote_ver) {
466  r.outcome = CANNOT_SATISFY;
467 
468  // TODO: Figure out how to ask the add-on manager for the user-friendly name of this add-on.
469  r.message = vgettext("The host's version of <i>$addon</i> is incompatible. They have version <b>$host_ver</b> while you have version <b>$local_ver</b>.", {
470  {"addon", r.addon_id},
471  {"host_ver", remote_ver.str()},
472  {"local_ver", local_ver.str()}
473  });
474 
475  required_addons.push_back(r);
476  return r.outcome;
477  }
478 
479  // Check if our version is too out of date to play.
480  if(remote_min_ver > local_ver) {
481  r.outcome = NEED_DOWNLOAD;
482 
483  // TODO: Figure out how to ask the add-on manager for the user-friendly name of this add-on.
484  r.message = vgettext("Your version of <i>$addon</i> is incompatible. You have version <b>$local_ver</b> while the host has version <b>$host_ver</b>.", {
485  {"addon", r.addon_id},
486  {"host_ver", remote_ver.str()},
487  {"local_ver", local_ver.str()}
488  });
489 
490  required_addons.push_back(r);
491  return r.outcome;
492  }
493  }
494 
495  return SATISFIED;
496 }
497 
499 {
500  return !started && vacant_slots > 0;
501 }
502 
504 {
506 }
507 
509 {
510  switch(display_status) {
511  case game_info::CLEAN:
512  return "clean";
513  case game_info::NEW:
514  return "new";
515  case game_info::DELETED:
516  return "deleted";
517  case game_info::UPDATED:
518  return "updated";
519  default:
520  ERR_CF << "BAD display_status " << display_status << " in game " << id << "\n";
521  return "?";
522  }
523 }
524 
526 {
527  const std::string& s1 = map_info;
528  const std::string& s2 = name;
529  return std::search(s1.begin(), s1.end(), filter.begin(), filter.end(),
530  chars_equal_insensitive) != s1.end()
531  || std::search(s2.begin(), s2.end(), filter.begin(), filter.end(),
532  chars_equal_insensitive) != s2.end();
533 }
534 
535 }
std::string scenario
Definition: lobby_data.hpp:147
std::string map_size_info
Definition: lobby_data.hpp:150
config & child(config_key_type key, int n=0)
Returns the nth child with the given key, or a reference to an invalid config if there is none...
Definition: config.cpp:352
ADDON_REQ addons_outcome
Definition: lobby_data.hpp:197
std::vector< char_t > string
size_t index(const utf8::string &str, const size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:71
std::string status
Definition: lobby_data.hpp:159
void add_message(const time_t &timestamp, const std::string &user, const std::string &message)
Definition: lobby_data.cpp:61
std::string mod_info
Definition: lobby_data.hpp:153
std::string era()
Definition: game.cpp:700
#define ERR_CF
Definition: lobby_data.cpp:39
bool match_string_filter(const std::string &filter) const
Definition: lobby_data.cpp:525
bool is_authenticated()
Definition: game.cpp:181
unsigned int current_turn
Definition: lobby_data.hpp:163
std::deque< chat_message > history_
Definition: lobby_data.hpp:60
config & find_child(config_key_type key, const std::string &name, const std::string &value)
Returns the first child of tag key with a name attribute containing value.
Definition: config.cpp:715
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
New lexcical_cast header.
user_state state
Definition: lobby_data.hpp:129
bool can_join() const
Definition: lobby_data.cpp:498
child_itors child_range(config_key_type key)
Definition: config.cpp:295
bool shuffle_sides()
Definition: game.cpp:475
To lexical_cast_default(From value, To fallback=To())
Lexical cast converts one type to another with a fallback.
std::set< std::string > members_
Definition: lobby_data.hpp:95
std::string map_info
Definition: lobby_data.hpp:149
bool chars_equal_insensitive(char a, char b)
Definition: general.hpp:29
size_t vacant_slots
Definition: lobby_data.hpp:161
bool remote_scenario
Definition: lobby_data.hpp:148
bool empty() const
Definition: config.cpp:750
To lexical_cast(From value)
Lexical cast converts one type to another.
Definitions for the interface to Wesnoth Markup Language (WML).
std::string name
Definition: lobby_data.hpp:146
std::string time_limit
Definition: lobby_data.hpp:160
static lg::log_domain log_lobby("lobby")
bool fog()
Definition: game.cpp:540
Pubic entry points for the MP workflow.
Definition: lobby_data.cpp:48
user_info(const config &c)
Definition: lobby_data.cpp:107
static UNUSEDNOWARN std::string _(const char *str)
Definition: gettext.hpp:89
bool is_member(const std::string &user) const
Definition: lobby_data.cpp:83
bool is_friend(const std::string &nick)
Definition: game.cpp:289
std::ostringstream wrapper.
Definition: formatter.hpp:38
int w() const
Effective map width.
Definition: map.hpp:90
std::string vision
Definition: lobby_data.hpp:158
chat_message(const time_t &timestamp, const std::string &user, const std::string &message)
Create a chat message.
Definition: lobby_data.cpp:50
const std::string unicode_multiplication_sign
Definition: constants.cpp:41
Encapsulates the map of the game.
Definition: map.hpp:34
static UNUSEDNOWARN std::string _n(const char *str1, const char *str2, int n)
Definition: gettext.hpp:93
static lg::log_domain log_config("config")
std::string map_data
Definition: lobby_data.hpp:145
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:148
bool registered_users_only()
Definition: game.cpp:465
std::string era_short
Definition: lobby_data.hpp:152
bool can_observe() const
Definition: lobby_data.cpp:503
bool is_ignored(const std::string &nick)
Definition: game.cpp:302
std::string read_map(const std::string &name)
std::string str() const
Serializes the version number into string form.
Definition: version.cpp:92
Helper class, don't construct this directly.
std::string login()
std::string name
Definition: lobby_data.hpp:126
bool has_attribute(config_key_type key) const
Definition: config.cpp:196
std::string dev_message
The message for developers telling which problem was triggered, this shouldn't be translated...
bool shroud()
Definition: game.cpp:550
static lg::log_domain log_engine("engine")
int h() const
Effective map height.
Definition: map.hpp:93
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
const char * display_status_string() const
Definition: lobby_data.cpp:508
Game configuration data as global variables.
Definition: build_info.cpp:53
const config & child_or_empty(config_key_type key) const
Returns the first child with the given key, or an empty config if there is none.
Definition: config.cpp:385
ADDON_REQ check_addon_version_compatibility(const config &local_item, const config &game)
Definition: lobby_data.cpp:441
bool use_map_settings()
Definition: game.cpp:493
#define i
This class represents the information a client has about a room.
Definition: lobby_data.hpp:66
Declarations for File-IO.
Represents version numbers.
Definition: version.hpp:43
std::string era
Definition: lobby_data.hpp:151
std::string vgettext(const char *msgid, const utils::string_map &symbols)
void update_state(int selected_game_id, const room_info *current_room=nullptr)
Definition: lobby_data.cpp:118
room_info(const std::string &name)
Definition: lobby_data.cpp:79
bool find(E event, F functor)
Tests whether an event handler is available.
static bool timestamp
Definition: log.cpp:46
const std::string unicode_em_dash
Definition: constants.cpp:39
void remove_member(const std::string &user)
Definition: lobby_data.cpp:93
Standard logging facilities (interface).
std::string message
Definition: exceptions.hpp:31
game_display_status display_status
Definition: lobby_data.hpp:186
game_info(const config &c, const config &game_config, const std::vector< std::string > &installed_addons)
Definition: lobby_data.cpp:173
static const char * name(const std::vector< SDL_Joystick * > &joysticks, const size_t index)
Definition: joystick.cpp:48
std::vector< required_addon > required_addons
Definition: lobby_data.hpp:196
void update_relation()
Definition: lobby_data.cpp:137
#define e
user_relation relation
Definition: lobby_data.hpp:128
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:93
mock_char c
Interfaces for manipulating version numbers of engine, add-ons, etc.
static std::string make_short_name(const std::string &long_name)
Definition: lobby_data.cpp:151
void process_room_members(const config &data)
Definition: lobby_data.cpp:98
void add_member(const std::string &user)
Definition: lobby_data.cpp:88