The Battle for Wesnoth  1.17.0-dev
lobby_data.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2021
3  by Tomasz Sniatowski <kailoran@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
17 
18 #include "config.hpp"
19 #include "filesystem.hpp"
20 #include "font/pango/escape.hpp"
21 #include "formatter.hpp"
22 #include "formula/string_utils.hpp"
23 #include "game_config_manager.hpp"
24 #include "game_config_view.hpp"
26 #include "game_version.hpp"
27 #include "gettext.hpp"
29 #include "lexical_cast.hpp"
30 #include "log.hpp"
31 #include "map/exception.hpp"
32 #include "map/map.hpp"
33 #include "mp_game_settings.hpp"
35 #include "preferences/game.hpp"
36 #include "wml_exception.hpp"
37 
38 #include <iterator>
39 
40 #include <boost/algorithm/string.hpp>
41 
42 static lg::log_domain log_config("config");
43 #define ERR_CF LOG_STREAM(err, log_config)
44 static lg::log_domain log_engine("engine");
45 #define WRN_NG LOG_STREAM(warn, log_engine)
46 
47 static lg::log_domain log_lobby("lobby");
48 #define DBG_LB LOG_STREAM(info, log_lobby)
49 #define LOG_LB LOG_STREAM(info, log_lobby)
50 #define ERR_LB LOG_STREAM(err, log_lobby)
51 
52 namespace mp {
53 
55  const std::string& user,
56  const std::string& message)
57  : timestamp(timestamp), user(user), message(message)
58 {
59 }
60 
62 {
63 }
64 
65 void chat_session::add_message(const std::time_t& timestamp,
66  const std::string& user,
67  const std::string& message)
68 {
69  history_.emplace_back(timestamp, user, message);
70 }
71 
72 
73 void chat_session::add_message(const std::string& user, const std::string& message)
74 {
75  add_message(std::time(nullptr), user, message);
76 }
77 
79 {
80  history_.clear();
81 }
82 
83 room_info::room_info(const std::string& name) : name_(name), members_(), log_()
84 {
85 }
86 
87 bool room_info::is_member(const std::string& user) const
88 {
89  return members_.find(user) != members_.end();
90 }
91 
92 void room_info::add_member(const std::string& user)
93 {
94  members_.insert(user);
95 }
96 
97 void room_info::remove_member(const std::string& user)
98 {
99  members_.erase(user);
100 }
101 
103 {
104  members_.clear();
105  for(const auto & m : data.child_range("member"))
106  {
107  members_.insert(m["name"]);
108  }
109 }
110 
112  : name(c["name"])
113  , forum_id(c["forum_id"].to_int())
114  , game_id(c["game_id"])
115  , relation(user_relation::ME)
116  , state(game_id == 0 ? user_state::LOBBY : user_state::GAME)
117  , registered(c["registered"].to_bool())
118  , observing(c["status"] == "observing")
119  , moderator(c["moderator"].to_bool(false))
120 {
121  update_relation();
122 }
123 
124 void user_info::update_state(int selected_game_id)
125 {
126  if(game_id != 0) {
127  if(game_id == selected_game_id) {
129  } else {
131  }
132  } else {
134  }
135  update_relation();
136 }
137 
139 {
140  if(name == preferences::login()) {
142  } else if(preferences::is_ignored(name)) {
144  } else if(preferences::is_friend(name)) {
146  } else {
148  }
149 }
150 
151 bool user_info::operator<(const user_info& b) const
152 {
153  return relation < b.relation || (relation == b.relation && translation::icompare(name, b.name) < 0);
154 }
155 
156 namespace
157 {
158 const std::string& spaced_em_dash()
159 {
160  static const std::string res = " " + font::unicode_em_dash + " ";
161  return res;
162 }
163 
164 std::string make_game_type_marker(const std::string& text, bool color_for_missing)
165 {
166  if(color_for_missing) {
167  return formatter() << "<b><span color='#f00'>[" << text << "]</span></b> ";
168  } else {
169  return formatter() << "<b>[" << text << "]</b> ";
170  }
171 }
172 
173 } // end anon namespace
174 
175 game_info::game_info(const config& game, const std::vector<std::string>& installed_addons)
176  : id(game["id"])
177  , map_data(game["map_data"])
178  , name(font::escape_text(game["name"]))
179  , scenario()
180  , type_marker()
181  , remote_scenario(false)
182  , map_info()
183  , map_size_info()
184  , era()
185  , gold(game["mp_village_gold"])
186  , support(game["mp_village_support"])
187  , xp(game["experience_modifier"].str() + "%")
188  , vision()
189  , status()
190  , time_limit()
191  , vacant_slots()
192  , current_turn(0)
193  , reloaded(game["savegame"].to_enum<mp_game_settings::SAVED_GAME_MODE>(mp_game_settings::SAVED_GAME_MODE::NONE) != mp_game_settings::SAVED_GAME_MODE::NONE)
194  , started(false)
195  , fog(game["mp_fog"].to_bool())
196  , shroud(game["mp_shroud"].to_bool())
197  , observers(game["observer"].to_bool(true))
198  , shuffle_sides(game["shuffle_sides"].to_bool(true))
199  , use_map_settings(game["mp_use_map_settings"].to_bool())
200  , private_replay(game["private_replay"].to_bool())
201  , verified(true)
202  , password_required(game["password"].to_bool())
203  , have_era(true)
204  , have_all_mods(true)
205  , has_friends(false)
206  , has_ignored(false)
207  , display_status(disp_status::NEW)
208  , required_addons()
209  , addons_outcome(addon_req::SATISFIED)
210 {
212 
213  // Parse the list of addons required to join this game.
214  for(const config& addon : game.child_range("addon")) {
215  if(addon.has_attribute("id") && addon["require"].to_bool(false)) {
216  if(std::find(installed_addons.begin(), installed_addons.end(), addon["id"].str()) == installed_addons.end()) {
217  required_addon r;
218  r.addon_id = addon["id"].str();
220 
221  // Use addon name if provided, else fall back on the addon id.
222  if(addon.has_attribute("name")) {
223  r.message = VGETTEXT("Missing addon: $name", {{"name", addon["name"].str()}});
224  } else {
225  r.message = VGETTEXT("Missing addon: $id", {{"id", addon["id"].str()}});
226  }
227 
228  required_addons.push_back(std::move(r));
229 
232  }
233  }
234  }
235  }
236 
237  if(!game["mp_era"].empty()) {
238  const config& era_cfg = game_config.find_child("era", "id", game["mp_era"]);
239  const bool require = game["require_era"].to_bool(true);
240  if(era_cfg) {
241  era = era_cfg["name"].str();
242 
243  if(require) {
244  addon_req result = check_addon_version_compatibility(era_cfg, game);
245  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
246  }
247  } else {
248  have_era = !require;
249  era = game["mp_era_name"].str();
250  verified = false;
251 
252  if(!have_era) {
254  }
255  }
256  } else {
257  era = _("Unknown era");
258  verified = false;
259  }
260 
261  std::stringstream info_stream;
262  info_stream << era;
263 
264  for(const config& cfg : game.child_range("modification")) {
265  mod_info.emplace_back(cfg["name"].str(), true);
266  info_stream << ' ' << mod_info.back().first;
267 
268  if(cfg["require_modification"].to_bool(false)) {
269  if(const config& mod = game_config.find_child("modification", "id", cfg["id"])) {
270  addon_req result = check_addon_version_compatibility(mod, game);
271  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
272  } else {
273  have_all_mods = false;
274  mod_info.back().second = false;
275 
277  }
278  }
279  }
280 
281  std::sort(mod_info.begin(), mod_info.end(), [](const auto& lhs, const auto& rhs) {
282  return translation::icompare(lhs.first, rhs.first) < 0;
283  });
284 
285  info_stream << ' ';
286 
287  if(map_data.empty()) {
288  map_data = filesystem::read_map(game["mp_scenario"]);
289  }
290 
291  if(map_data.empty()) {
292  info_stream << " — ??×??";
293  } else {
294  try {
295  gamemap map(map_data);
296  std::ostringstream msi;
297  msi << map.w() << font::unicode_multiplication_sign << map.h();
298  map_size_info = msi.str();
299  info_stream << spaced_em_dash() << map_size_info;
300  } catch(const incorrect_map_format_error&) {
301  verified = false;
302  } catch(const wml_exception& e) {
303  ERR_CF << "map could not be loaded: " << e.dev_message << '\n';
304  verified = false;
305  }
306  }
307 
308  info_stream << " ";
309 
310  //
311  // Check scenarios and campaigns
312  //
313  if(!game["mp_scenario"].empty() && game["mp_campaign"].empty()) {
314  // Check if it's a multiplayer scenario
315  const config* level_cfg = &game_config.find_child("multiplayer", "id", game["mp_scenario"]);
316  const bool require = game["require_scenario"].to_bool(false);
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  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), false);
325  scenario = (*level_cfg)["name"].str();
326  info_stream << scenario;
327 
328  // Reloaded games do not match the original scenario hash, so it makes no sense
329  // to test them, since they always would appear as remote scenarios
330  if(!reloaded) {
331  if(const config& hashes = game_config.child("multiplayer_hashes")) {
332  std::string hash = game["hash"];
333  bool hash_found = false;
334  for(const auto & i : hashes.attribute_range()) {
335  if(i.first == game["mp_scenario"] && i.second == hash) {
336  hash_found = true;
337  break;
338  }
339  }
340 
341  if(!hash_found) {
342  remote_scenario = true;
343  info_stream << spaced_em_dash();
344  info_stream << _("Remote scenario");
345  verified = false;
346  }
347  }
348  }
349 
350  if(require) {
351  addon_req result = check_addon_version_compatibility((*level_cfg), game);
352  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
353  }
354  } else {
355  if(require) {
356  addons_outcome = std::max(addons_outcome, addon_req::NEED_DOWNLOAD); // Elevate to most severe error level encountered so far
357  }
358  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), true);
359  scenario = game["mp_scenario_name"].str();
360  info_stream << scenario;
361  verified = false;
362  }
363  } else if(!game["mp_campaign"].empty()) {
364  if(const config& campaign_cfg = game_config.find_child("campaign", "id", game["mp_campaign"])) {
365  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), false);
366 
367  std::stringstream campaign_text;
368  campaign_text
369  << campaign_cfg["name"] << spaced_em_dash()
370  << game["mp_scenario_name"];
371 
372  // Difficulty
373  config difficulties = gui2::dialogs::generate_difficulty_config(campaign_cfg);
374  for(const config& difficulty : difficulties.child_range("difficulty")) {
375  if(difficulty["define"] == game["difficulty_define"]) {
376  campaign_text << spaced_em_dash() << difficulty["description"];
377 
378  break;
379  }
380  }
381 
382  scenario = campaign_text.str();
383  info_stream << campaign_text.rdbuf();
384 
385  // TODO: should we have this?
386  //if(game["require_scenario"].to_bool(false)) {
387  addon_req result = check_addon_version_compatibility(campaign_cfg, game);
388  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
389  //}
390  } else {
391  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), true);
392  scenario = game["mp_campaign_name"].str();
393  info_stream << scenario;
394  verified = false;
395  }
396  } else {
397  scenario = _("Unknown scenario");
398  info_stream << scenario;
399  verified = false;
400  }
401 
402  // Remove any newlines that might have been in game names (the player-set ones)
403  // No idea how this could happen, but I've seen it (vultraz, 2020-10-26)
404  boost::erase_all(name, "\n");
405 
406  // Remove any newlines that might have been in game titles (scenario/campaign name, etc.)
407  boost::replace_all(scenario, "\n", " " + font::unicode_em_dash + " ");
408 
409  if(reloaded) {
410  info_stream << spaced_em_dash();
411  info_stream << _("Reloaded game");
412  verified = false;
413  }
414 
415  // These should always be present in the data the server sends, but may or may not be empty.
416  // I'm just using child_or_empty here to preempt any cases where they might not be included.
417  const config& s = game.child_or_empty("slot_data");
418  const config& t = game.child_or_empty("turn_data");
419 
420  if(!s.empty()) {
421  started = false;
422 
423  vacant_slots = s["vacant"].to_unsigned();
424 
425  if(vacant_slots > 0) {
426  status = formatter() << _n("Vacant Slot:", "Vacant Slots:", vacant_slots) << " " << vacant_slots << "/" << s["max"];
427  } else {
428  status = _("mp_game_available_slots^Full");
429  }
430  }
431 
432  if(!t.empty()) {
433  started = true;
434 
435  current_turn = t["current"].to_unsigned();
436  const int max_turns = t["max"].to_int();
437 
438  if(max_turns > -1) {
439  status = formatter() << _("Turn") << " " << t["current"] << "/" << max_turns;
440  } else {
441  status = formatter() << _("Turn") << " " << t["current"];
442  }
443  }
444 
445  if(fog) {
446  vision = _("Fog");
447  if(shroud) {
448  vision += "/";
449  vision += _("Shroud");
450  }
451  } else if(shroud) {
452  vision = _("Shroud");
453  } else {
454  vision = _("vision^none");
455  }
456 
457  if(game["mp_countdown"].to_bool()) {
459  << game["mp_countdown_init_time"].str() << "+"
460  << game["mp_countdown_turn_bonus"].str() << "/"
461  << game["mp_countdown_action_bonus"].str();
462  } else {
463  time_limit = _("time limit^none");
464  }
465 
466  map_info = info_stream.str();
467 }
468 
470 {
471  if(!local_item.has_attribute("addon_id") || !local_item.has_attribute("addon_version")) {
472  return addon_req::SATISFIED;
473  }
474 
475  if(const config& game_req = game.find_child("addon", "id", local_item["addon_id"])) {
476  if(!game_req["require"].to_bool(false)) {
477  return addon_req::SATISFIED;
478  }
479 
480  required_addon r{local_item["addon_id"].str(), addon_req::SATISFIED, ""};
481 
482  // Local version
483  const version_info local_ver(local_item["addon_version"].str());
484  version_info local_min_ver(local_item.has_attribute("addon_min_version") ? local_item["addon_min_version"] : local_item["addon_version"]);
485 
486  // If the UMC didn't specify last compatible version, assume no backwards compatibility.
487  // Also apply some sanity checking regarding min version; if the min ver doesn't make sense, ignore it.
488  local_min_ver = std::min(local_min_ver, local_ver);
489 
490  // Remote version
491  const version_info remote_ver(game_req["version"].str());
492  version_info remote_min_ver(game_req.has_attribute("min_version") ? game_req["min_version"] : game_req["version"]);
493 
494  remote_min_ver = std::min(remote_min_ver, remote_ver);
495 
496  // Check if the host is too out of date to play.
497  if(local_min_ver > remote_ver) {
498  DBG_LB << "r.outcome = CANNOT_SATISFY for item='" << local_item["id"]
499  << "' addon='" << local_item["addon_id"]
500  << "' addon_min_version='" << local_item["addon_min_version"]
501  << "' addon_min_version_parsed='" << local_min_ver.str()
502  << "' addon_version='" << local_item["addon_version"]
503  << "' remote_ver='" << remote_ver.str()
504  << "'\n";
505  r.outcome = addon_req::CANNOT_SATISFY;
506 
507  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>.", {
508  {"addon", local_item["addon_title"].str()},
509  {"host_ver", remote_ver.str()},
510  {"local_ver", local_ver.str()}
511  });
512 
513  required_addons.push_back(r);
514  return r.outcome;
515  }
516 
517  // Check if our version is too out of date to play.
518  if(remote_min_ver > local_ver) {
519  r.outcome = addon_req::NEED_DOWNLOAD;
520 
521  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>.", {
522  {"addon", local_item["addon_title"].str()},
523  {"host_ver", remote_ver.str()},
524  {"local_ver", local_ver.str()}
525  });
526 
527  required_addons.push_back(r);
528  return r.outcome;
529  }
530  }
531 
532  return addon_req::SATISFIED;
533 }
534 
536 {
537  return !started && vacant_slots > 0;
538 }
539 
541 {
543 }
544 
546 {
547  switch(display_status) {
549  return "clean";
551  return "new";
553  return "deleted";
555  return "updated";
556  default:
557  ERR_CF << "BAD display_status " << static_cast<int>(display_status) << " in game " << id << "\n";
558  return "?";
559  }
560 }
561 
562 bool game_info::match_string_filter(const std::string& filter) const
563 {
564  const std::string& s1 = name;
565  const std::string& s2 = map_info;
566  return std::search(s1.begin(), s1.end(), filter.begin(), filter.end(),
567  utils::chars_equal_insensitive) != s1.end()
568  || std::search(s2.begin(), s2.end(), filter.begin(), filter.end(),
569  utils::chars_equal_insensitive) != s2.end();
570 }
571 
572 }
std::string scenario
Definition: lobby_data.hpp:151
std::string map_size_info
Definition: lobby_data.hpp:155
static std::string _n(const char *str1, const char *str2, int n)
Definition: gettext.hpp:97
void add_message(const std::time_t &timestamp, const std::string &user, const std::string &message)
Definition: lobby_data.cpp:65
std::string status
Definition: lobby_data.hpp:165
std::string era()
Definition: game.cpp:695
#define ERR_CF
Definition: lobby_data.cpp:43
Interfaces for manipulating version numbers of engine, add-ons, etc.
std::vector< std::pair< std::string, bool > > mod_info
List of modification names and whether they&#39;re installed or not.
Definition: lobby_data.hpp:159
Collection of helper functions relating to Pango formatting.
unsigned int current_turn
Definition: lobby_data.hpp:169
std::deque< chat_message > history_
Definition: lobby_data.hpp:62
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:885
Add a special kind of assert to validate whether the input from WML doesn&#39;t contain any problems that...
New lexcical_cast header.
bool has_attribute(config_key_type key) const
Definition: config.cpp:211
user_state state
Definition: lobby_data.hpp:132
addon_req addons_outcome
Definition: lobby_data.hpp:204
child_itors child_range(config_key_type key)
Definition: config.cpp:344
bool shuffle_sides()
Definition: game.cpp:475
std::set< std::string > members_
Definition: lobby_data.hpp:97
bool chars_equal_insensitive(char a, char b)
Definition: general.hpp:23
std::string map_info
Definition: lobby_data.hpp:154
bool remote_scenario
Definition: lobby_data.hpp:153
static std::string _(const char *str)
Definition: gettext.hpp:93
Definitions for the interface to Wesnoth Markup Language (WML).
std::string name
Definition: lobby_data.hpp:150
std::string time_limit
Definition: lobby_data.hpp:166
#define b
bool logged_in_as_moderator()
Gets whether the currently logged-in user is a moderator.
static lg::log_domain log_lobby("lobby")
bool fog()
Definition: game.cpp:539
Main entry points of multiplayer mode.
Definition: lobby_data.cpp:52
static game_config_manager * get()
user_info(const config &c)
Definition: lobby_data.cpp:111
chat_message(const std::time_t &timestamp, const std::string &user, const std::string &message)
Create a chat message.
Definition: lobby_data.cpp:54
bool is_friend(const std::string &nick)
Definition: game.cpp:278
int w() const
Effective map width.
Definition: map.hpp:50
std::ostringstream wrapper.
Definition: formatter.hpp:39
#define DBG_LB
Definition: lobby_data.cpp:48
const game_config_view & game_config() const
std::string vision
Definition: lobby_data.hpp:164
const std::string unicode_multiplication_sign
Definition: constants.cpp:46
Encapsulates the map of the game.
Definition: map.hpp:171
static lg::log_domain log_config("config")
void update_state(int selected_game_id)
Definition: lobby_data.cpp:124
disp_status display_status
Definition: lobby_data.hpp:193
std::string map_data
Definition: lobby_data.hpp:149
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:178
bool is_member(const std::string &user) const
Definition: lobby_data.cpp:87
bool is_ignored(const std::string &nick)
Definition: game.cpp:290
std::string read_map(const std::string &name)
Helper class, don&#39;t construct this directly.
std::string login()
std::string name
Definition: lobby_data.hpp:128
std::string dev_message
The message for developers telling which problem was triggered, this shouldn&#39;t be translated...
bool shroud()
Definition: game.cpp:549
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:215
std::string escape_text(const std::string &text)
Escapes the pango markup characters in a text.
Definition: escape.hpp:33
const char * display_status_string() const
Definition: lobby_data.cpp:545
std::size_t i
Definition: function.cpp:967
static lg::log_domain log_engine("engine")
Default, unset return value.
Definition: retval.hpp:32
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
Game configuration data as global variables.
Definition: build_info.cpp:59
static map_location::DIRECTION s
std::string type_marker
Definition: lobby_data.hpp:152
bool use_map_settings()
Definition: game.cpp:495
addon_req check_addon_version_compatibility(const config &local_item, const config &game)
Definition: lobby_data.cpp:469
Declarations for File-IO.
bool can_observe() const
Definition: lobby_data.cpp:540
static int sort(lua_State *L)
Definition: ltablib.cpp:397
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
Represents version numbers.
std::string era
Definition: lobby_data.hpp:156
const config & find_child(config_key_type key, const std::string &name, const std::string &value) const
This class represents the information a client has about another player.
Definition: lobby_data.hpp:105
room_info(const std::string &name)
Definition: lobby_data.cpp:83
double t
Definition: astarsearch.cpp:65
static bool timestamp
Definition: log.cpp:44
int icompare(const std::string &s1, const std::string &s2)
Case-insensitive lexicographical comparison.
Definition: gettext.cpp:491
std::size_t vacant_slots
Definition: lobby_data.hpp:167
const std::string unicode_em_dash
Definition: constants.cpp:44
void remove_member(const std::string &user)
Definition: lobby_data.cpp:97
Standard logging facilities (interface).
std::string str() const
Serializes the version number into string form.
std::vector< required_addon > required_addons
Definition: lobby_data.hpp:203
void update_relation()
Definition: lobby_data.cpp:138
#define e
game_info(const config &c, const std::vector< std::string > &installed_addons)
Definition: lobby_data.cpp:175
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:465
user_relation relation
Definition: lobby_data.hpp:131
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:61
bool can_join() const
Definition: lobby_data.cpp:535
mock_char c
int h() const
Effective map height.
Definition: map.hpp:53
bool operator<(const user_info &b) const
Definition: lobby_data.cpp:151
bool empty() const
Definition: config.cpp:941
const config & child(config_key_type key) const
void process_room_members(const config &data)
Definition: lobby_data.cpp:102
bool match_string_filter(const std::string &filter) const
Definition: lobby_data.cpp:562
const std::string & name() const
Definition: lobby_data.hpp:73
void add_member(const std::string &user)
Definition: lobby_data.cpp:92