The Battle for Wesnoth  1.17.4+dev
lobby_data.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2022
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  : name(c["name"])
56  , forum_id(c["forum_id"].to_int())
57  , game_id(c["game_id"])
58  , registered(c["registered"].to_bool())
59  , observing(c["status"] == "observing")
60  , moderator(c["moderator"].to_bool(false))
61 {
62 }
63 
64 user_info::user_state user_info::get_state(int selected_game_id) const
65 {
66  if(game_id == 0) {
67  return user_state::LOBBY;
68  } else if(game_id == selected_game_id) {
69  return user_state::SEL_GAME;
70  } else {
71  return user_state::GAME;
72  }
73 }
74 
76 {
77  if(name == preferences::login()) {
78  return user_relation::ME;
79  } else if(preferences::is_ignored(name)) {
81  } else if(preferences::is_friend(name)) {
82  return user_relation::FRIEND;
83  } else {
85  }
86 }
87 
88 bool user_info::operator<(const user_info& b) const
89 {
90  const auto ar = get_relation();
91  const auto br = b.get_relation();
92  return ar < br || (ar == br && translation::icompare(name, b.name) < 0);
93 }
94 
95 namespace
96 {
97 const std::string& spaced_em_dash()
98 {
99  static const std::string res = " " + font::unicode_em_dash + " ";
100  return res;
101 }
102 
103 std::string make_game_type_marker(const std::string& text, bool color_for_missing)
104 {
105  if(color_for_missing) {
106  return formatter() << "<b><span color='#f00'>[" << text << "]</span></b> ";
107  } else {
108  return formatter() << "<b>[" << text << "]</b> ";
109  }
110 }
111 
112 } // end anon namespace
113 
114 game_info::game_info(const config& game, const std::vector<std::string>& installed_addons)
115  : id(game["id"])
116  , map_data(game["map_data"])
117  , name(font::escape_text(game["name"]))
118  , scenario()
119  , type_marker()
120  , remote_scenario(false)
121  , map_info()
122  , map_size_info()
123  , era()
124  , gold(game["mp_village_gold"])
125  , support(game["mp_village_support"])
126  , xp(game["experience_modifier"].str() + "%")
127  , vision()
128  , status()
129  , time_limit()
130  , vacant_slots()
131  , current_turn(0)
132  , reloaded(saved_game_mode::get_enum(game["savegame"].str()).value_or(saved_game_mode::type::no) != saved_game_mode::type::no)
133  , started(false)
134  , fog(game["mp_fog"].to_bool())
135  , shroud(game["mp_shroud"].to_bool())
136  , observers(game["observer"].to_bool(true))
137  , shuffle_sides(game["shuffle_sides"].to_bool(true))
138  , use_map_settings(game["mp_use_map_settings"].to_bool())
139  , private_replay(game["private_replay"].to_bool())
140  , verified(true)
141  , password_required(game["password"].to_bool())
142  , have_era(true)
143  , have_all_mods(true)
144  , has_friends(false)
145  , has_ignored(false)
146  , display_status(disp_status::NEW)
147  , required_addons()
148  , addons_outcome(addon_req::SATISFIED)
149 {
151 
152  // Parse the list of addons required to join this game.
153  for(const config& addon : game.child_range("addon")) {
154  if(addon.has_attribute("id") && addon["require"].to_bool(false)) {
155  if(std::find(installed_addons.begin(), installed_addons.end(), addon["id"].str()) == installed_addons.end()) {
156  required_addon r;
157  r.addon_id = addon["id"].str();
159 
160  // Use addon name if provided, else fall back on the addon id.
161  if(addon.has_attribute("name")) {
162  r.message = VGETTEXT("Missing addon: $name", {{"name", addon["name"].str()}});
163  } else {
164  r.message = VGETTEXT("Missing addon: $id", {{"id", addon["id"].str()}});
165  }
166 
167  required_addons.push_back(std::move(r));
168 
171  }
172  }
173  }
174  }
175 
176  if(!game["mp_era"].empty()) {
177  const config& era_cfg = game_config.find_child("era", "id", game["mp_era"]);
178  const bool require = game["require_era"].to_bool(true);
179  if(era_cfg) {
180  era = era_cfg["name"].str();
181 
182  if(require) {
183  addon_req result = check_addon_version_compatibility(era_cfg, game);
184  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
185  }
186  } else {
187  have_era = !require;
188  era = game["mp_era_name"].str();
189  verified = false;
190 
191  if(!have_era) {
193  }
194  }
195  } else {
196  era = _("Unknown era");
197  verified = false;
198  }
199 
200  std::stringstream info_stream;
201  info_stream << era;
202 
203  for(const config& cfg : game.child_range("modification")) {
204  mod_info.emplace_back(cfg["name"].str(), true);
205  info_stream << ' ' << mod_info.back().first;
206 
207  if(cfg["require_modification"].to_bool(false)) {
208  if(const config& mod = game_config.find_child("modification", "id", cfg["id"])) {
209  addon_req result = check_addon_version_compatibility(mod, game);
210  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
211  } else {
212  have_all_mods = false;
213  mod_info.back().second = false;
214 
216  }
217  }
218  }
219 
220  std::sort(mod_info.begin(), mod_info.end(), [](const auto& lhs, const auto& rhs) {
221  return translation::icompare(lhs.first, rhs.first) < 0;
222  });
223 
224  info_stream << ' ';
225 
226  if(map_data.empty()) {
227  map_data = filesystem::read_map(game["mp_scenario"]);
228  }
229 
230  if(map_data.empty()) {
231  info_stream << " — ??×??";
232  } else {
233  try {
234  gamemap map(map_data);
235  std::ostringstream msi;
236  msi << map.w() << font::unicode_multiplication_sign << map.h();
237  map_size_info = msi.str();
238  info_stream << spaced_em_dash() << map_size_info;
239  } catch(const incorrect_map_format_error&) {
240  verified = false;
241  } catch(const wml_exception& e) {
242  ERR_CF << "map could not be loaded: " << e.dev_message << '\n';
243  verified = false;
244  }
245  }
246 
247  info_stream << " ";
248 
249  //
250  // Check scenarios and campaigns
251  //
252  if(!game["mp_scenario"].empty() && game["mp_campaign"].empty()) {
253  // Check if it's a multiplayer scenario
254  const config* level_cfg = &game_config.find_child("multiplayer", "id", game["mp_scenario"]);
255  const bool require = game["require_scenario"].to_bool(false);
256 
257  // Check if it's a user map
258  if(!*level_cfg) {
259  level_cfg = &game_config.find_child("generic_multiplayer", "id", game["mp_scenario"]);
260  }
261 
262  if(*level_cfg) {
263  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), false);
264  scenario = (*level_cfg)["name"].str();
265  info_stream << scenario;
266 
267  // Reloaded games do not match the original scenario hash, so it makes no sense
268  // to test them, since they always would appear as remote scenarios
269  if(!reloaded) {
270  if(const config& hashes = game_config.child("multiplayer_hashes")) {
271  std::string hash = game["hash"];
272  bool hash_found = false;
273  for(const auto & i : hashes.attribute_range()) {
274  if(i.first == game["mp_scenario"] && i.second == hash) {
275  hash_found = true;
276  break;
277  }
278  }
279 
280  if(!hash_found) {
281  remote_scenario = true;
282  info_stream << spaced_em_dash();
283  info_stream << _("Remote scenario");
284  verified = false;
285  }
286  }
287  }
288 
289  if(require) {
290  addon_req result = check_addon_version_compatibility((*level_cfg), game);
291  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
292  }
293  } else {
294  if(require) {
295  addons_outcome = std::max(addons_outcome, addon_req::NEED_DOWNLOAD); // Elevate to most severe error level encountered so far
296  }
297  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), true);
298  scenario = game["mp_scenario_name"].str();
299  info_stream << scenario;
300  verified = false;
301  }
302  } else if(!game["mp_campaign"].empty()) {
303  if(const config& campaign_cfg = game_config.find_child("campaign", "id", game["mp_campaign"])) {
304  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), false);
305 
306  std::stringstream campaign_text;
307  campaign_text
308  << campaign_cfg["name"] << spaced_em_dash()
309  << game["mp_scenario_name"];
310 
311  // Difficulty
312  config difficulties = gui2::dialogs::generate_difficulty_config(campaign_cfg);
313  for(const config& difficulty : difficulties.child_range("difficulty")) {
314  if(difficulty["define"] == game["difficulty_define"]) {
315  campaign_text << spaced_em_dash() << difficulty["description"];
316 
317  break;
318  }
319  }
320 
321  scenario = campaign_text.str();
322  info_stream << campaign_text.rdbuf();
323 
324  // TODO: should we have this?
325  //if(game["require_scenario"].to_bool(false)) {
326  addon_req result = check_addon_version_compatibility(campaign_cfg, game);
327  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
328  //}
329  } else {
330  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), true);
331  scenario = game["mp_campaign_name"].str();
332  info_stream << scenario;
333  verified = false;
334  }
335  } else {
336  scenario = _("Unknown scenario");
337  info_stream << scenario;
338  verified = false;
339  }
340 
341  // Remove any newlines that might have been in game names (the player-set ones)
342  // No idea how this could happen, but I've seen it (vultraz, 2020-10-26)
343  boost::erase_all(name, "\n");
344 
345  // Remove any newlines that might have been in game titles (scenario/campaign name, etc.)
346  boost::replace_all(scenario, "\n", " " + font::unicode_em_dash + " ");
347 
348  if(reloaded) {
349  info_stream << spaced_em_dash();
350  info_stream << _("Reloaded game");
351  verified = false;
352  }
353 
354  // These should always be present in the data the server sends, but may or may not be empty.
355  // I'm just using child_or_empty here to preempt any cases where they might not be included.
356  const config& s = game.child_or_empty("slot_data");
357  const config& t = game.child_or_empty("turn_data");
358 
359  if(!s.empty()) {
360  started = false;
361 
362  vacant_slots = s["vacant"].to_unsigned();
363 
364  if(vacant_slots > 0) {
365  status = formatter() << _n("Vacant Slot:", "Vacant Slots:", vacant_slots) << " " << vacant_slots << "/" << s["max"];
366  } else {
367  status = _("mp_game_available_slots^Full");
368  }
369  }
370 
371  if(!t.empty()) {
372  started = true;
373 
374  current_turn = t["current"].to_unsigned();
375  const int max_turns = t["max"].to_int();
376 
377  if(max_turns > -1) {
378  status = formatter() << _("Turn") << " " << t["current"] << "/" << max_turns;
379  } else {
380  status = formatter() << _("Turn") << " " << t["current"];
381  }
382  }
383 
384  if(fog) {
385  vision = _("Fog");
386  if(shroud) {
387  vision += "/";
388  vision += _("Shroud");
389  }
390  } else if(shroud) {
391  vision = _("Shroud");
392  } else {
393  vision = _("vision^none");
394  }
395 
396  if(game["mp_countdown"].to_bool()) {
398  << game["mp_countdown_init_time"].str() << "+"
399  << game["mp_countdown_turn_bonus"].str() << "/"
400  << game["mp_countdown_action_bonus"].str();
401  } else {
402  time_limit = _("time limit^none");
403  }
404 
405  map_info = info_stream.str();
406 }
407 
409 {
410  if(!local_item.has_attribute("addon_id") || !local_item.has_attribute("addon_version")) {
411  return addon_req::SATISFIED;
412  }
413 
414  if(const config& game_req = game.find_child("addon", "id", local_item["addon_id"])) {
415  if(!game_req["require"].to_bool(false)) {
416  return addon_req::SATISFIED;
417  }
418 
419  required_addon r{local_item["addon_id"].str(), addon_req::SATISFIED, ""};
420 
421  // Local version
422  const version_info local_ver(local_item["addon_version"].str());
423  version_info local_min_ver(local_item.has_attribute("addon_min_version") ? local_item["addon_min_version"] : local_item["addon_version"]);
424 
425  // If the UMC didn't specify last compatible version, assume no backwards compatibility.
426  // Also apply some sanity checking regarding min version; if the min ver doesn't make sense, ignore it.
427  local_min_ver = std::min(local_min_ver, local_ver);
428 
429  // Remote version
430  const version_info remote_ver(game_req["version"].str());
431  version_info remote_min_ver(game_req.has_attribute("min_version") ? game_req["min_version"] : game_req["version"]);
432 
433  remote_min_ver = std::min(remote_min_ver, remote_ver);
434 
435  // Check if the host is too out of date to play.
436  if(local_min_ver > remote_ver) {
437  DBG_LB << "r.outcome = CANNOT_SATISFY for item='" << local_item["id"]
438  << "' addon='" << local_item["addon_id"]
439  << "' addon_min_version='" << local_item["addon_min_version"]
440  << "' addon_min_version_parsed='" << local_min_ver.str()
441  << "' addon_version='" << local_item["addon_version"]
442  << "' remote_ver='" << remote_ver.str()
443  << "'\n";
444  r.outcome = addon_req::CANNOT_SATISFY;
445 
446  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>.", {
447  {"addon", local_item["addon_title"].str()},
448  {"host_ver", remote_ver.str()},
449  {"local_ver", local_ver.str()}
450  });
451 
452  required_addons.push_back(r);
453  return r.outcome;
454  }
455 
456  // Check if our version is too out of date to play.
457  if(remote_min_ver > local_ver) {
458  r.outcome = addon_req::NEED_DOWNLOAD;
459 
460  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>.", {
461  {"addon", local_item["addon_title"].str()},
462  {"host_ver", remote_ver.str()},
463  {"local_ver", local_ver.str()}
464  });
465 
466  required_addons.push_back(r);
467  return r.outcome;
468  }
469  }
470 
471  return addon_req::SATISFIED;
472 }
473 
475 {
476  return !started && vacant_slots > 0;
477 }
478 
480 {
482 }
483 
485 {
486  switch(display_status) {
488  return "clean";
490  return "new";
492  return "deleted";
494  return "updated";
495  default:
496  ERR_CF << "BAD display_status " << static_cast<int>(display_status) << " in game " << id << "\n";
497  return "?";
498  }
499 }
500 
501 bool game_info::match_string_filter(const std::string& filter) const
502 {
503  const std::string& s1 = name;
504  const std::string& s2 = map_info;
505  return std::search(s1.begin(), s1.end(), filter.begin(), filter.end(),
506  utils::chars_equal_insensitive) != s1.end()
507  || std::search(s2.begin(), s2.end(), filter.begin(), filter.end(),
508  utils::chars_equal_insensitive) != s2.end();
509 }
510 
511 }
std::string scenario
Definition: lobby_data.hpp:75
std::string map_size_info
Definition: lobby_data.hpp:79
static std::string _n(const char *str1, const char *str2, int n)
Definition: gettext.hpp:97
std::string status
Definition: lobby_data.hpp:89
std::string era()
Definition: game.cpp:688
#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:83
Collection of helper functions relating to Pango formatting.
unsigned int current_turn
Definition: lobby_data.hpp:93
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
addon_req addons_outcome
Definition: lobby_data.hpp:128
child_itors child_range(config_key_type key)
Definition: config.cpp:344
bool shuffle_sides()
Definition: game.cpp:468
user_state get_state(int selected_game_id) const
Definition: lobby_data.cpp:64
bool chars_equal_insensitive(char a, char b)
Definition: general.hpp:23
std::string map_info
Definition: lobby_data.hpp:78
bool remote_scenario
Definition: lobby_data.hpp:77
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:74
std::string time_limit
Definition: lobby_data.hpp:90
#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:532
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:54
bool is_friend(const std::string &nick)
Definition: game.cpp:271
int w() const
Effective map width.
Definition: map.hpp:50
user_relation get_relation() const
Definition: lobby_data.cpp:75
The base template for associating string values with enum values.
Definition: enum_base.hpp:30
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:88
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")
disp_status display_status
Definition: lobby_data.hpp:117
std::string map_data
Definition: lobby_data.hpp:73
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:178
bool is_ignored(const std::string &nick)
Definition: game.cpp:283
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:54
std::string dev_message
The message for developers telling which problem was triggered, this shouldn&#39;t be translated...
bool shroud()
Definition: game.cpp:542
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:484
std::size_t i
Definition: function.cpp:967
static lg::log_domain log_engine("engine")
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
Game configuration data as global variables.
Definition: build_info.cpp:60
static map_location::DIRECTION s
std::string type_marker
Definition: lobby_data.hpp:76
bool use_map_settings()
Definition: game.cpp:488
addon_req check_addon_version_compatibility(const config &local_item, const config &game)
Definition: lobby_data.cpp:408
Declarations for File-IO.
bool can_observe() const
Definition: lobby_data.cpp:479
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
Represents version numbers.
std::string era
Definition: lobby_data.hpp:80
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:32
double t
Definition: astarsearch.cpp:65
int icompare(const std::string &s1, const std::string &s2)
Case-insensitive lexicographical comparison.
Definition: gettext.cpp:516
std::size_t vacant_slots
Definition: lobby_data.hpp:91
const std::string unicode_em_dash
Definition: constants.cpp:44
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:127
#define e
game_info(const config &c, const std::vector< std::string > &installed_addons)
Definition: lobby_data.cpp:114
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
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:60
bool can_join() const
Definition: lobby_data.cpp:474
mock_char c
int h() const
Effective map height.
Definition: map.hpp:53
bool operator<(const user_info &b) const
Definition: lobby_data.cpp:88
bool empty() const
Definition: config.cpp:941
const config & child(config_key_type key) const
bool match_string_filter(const std::string &filter) const
Definition: lobby_data.cpp:501