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