The Battle for Wesnoth  1.17.21+dev
lobby_data.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2023
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  , auto_hosted(game["auto_hosted"].to_bool())
147  , display_status(disp_status::NEW)
148  , required_addons()
149  , addons_outcome(addon_req::SATISFIED)
150 {
152 
153  // Parse the list of addons required to join this game.
154  for(const config& addon : game.child_range("addon")) {
155  if(addon.has_attribute("id") && addon["require"].to_bool(false)) {
156  if(std::find(installed_addons.begin(), installed_addons.end(), addon["id"].str()) == installed_addons.end()) {
157  required_addon r;
158  r.addon_id = addon["id"].str();
160 
161  // Use addon name if provided, else fall back on the addon id.
162  if(addon.has_attribute("name")) {
163  r.message = VGETTEXT("Missing addon: $name", {{"name", addon["name"].str()}});
164  } else {
165  r.message = VGETTEXT("Missing addon: $id", {{"id", addon["id"].str()}});
166  }
167 
168  required_addons.push_back(std::move(r));
169 
172  }
173  }
174  }
175  }
176 
177  if(!game["mp_era"].empty()) {
178  auto era_cfg = game_config.find_child("era", "id", game["mp_era"]);
179  const bool require = game["require_era"].to_bool(true);
180  if(era_cfg) {
181  era = era_cfg["name"].str();
182 
183  if(require) {
185  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
186  }
187  } else {
188  have_era = !require;
189  era = game["mp_era_name"].str();
190  verified = false;
191 
192  if(!have_era) {
194  }
195  }
196  } else {
197  era = _("Unknown era");
198  verified = false;
199  }
200 
201  std::stringstream info_stream;
202  info_stream << era;
203 
204  for(const config& cfg : game.child_range("modification")) {
205  mod_info.emplace_back(cfg["name"].str(), true);
206  info_stream << ' ' << mod_info.back().first;
207 
208  if(cfg["require_modification"].to_bool(false)) {
209  if(auto mod = game_config.find_child("modification", "id", cfg["id"])) {
211  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
212  } else {
213  have_all_mods = false;
214  mod_info.back().second = false;
215 
217  }
218  }
219  }
220 
221  std::sort(mod_info.begin(), mod_info.end(), [](const auto& lhs, const auto& rhs) {
222  return translation::icompare(lhs.first, rhs.first) < 0;
223  });
224 
225  info_stream << ' ';
226 
227  if(map_data.empty()) {
228  map_data = filesystem::read_map(game["mp_scenario"]);
229  }
230 
231  if(map_data.empty()) {
232  info_stream << " — ??×??";
233  } else {
234  try {
235  gamemap map(map_data);
236  std::ostringstream msi;
237  msi << map.w() << font::unicode_multiplication_sign << map.h();
238  map_size_info = msi.str();
239  info_stream << spaced_em_dash() << map_size_info;
240  } catch(const incorrect_map_format_error&) {
241  verified = false;
242  } catch(const wml_exception& e) {
243  ERR_CF << "map could not be loaded: " << e.dev_message;
244  verified = false;
245  }
246  }
247 
248  info_stream << " ";
249 
250  //
251  // Check scenarios and campaigns
252  //
253  if(!game["mp_scenario"].empty() && game["mp_campaign"].empty()) {
254  // Check if it's a multiplayer scenario
255  const config* level_cfg = game_config.find_child("multiplayer", "id", game["mp_scenario"]).ptr();
256  const bool require = game["require_scenario"].to_bool(false);
257 
258  // Check if it's a user map
259  if(level_cfg) {
260  level_cfg = game_config.find_child("generic_multiplayer", "id", game["mp_scenario"]).ptr();
261  }
262 
263  if(level_cfg) {
264  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), false);
265  scenario = (*level_cfg)["name"].str();
266  info_stream << scenario;
267 
268  // Reloaded games do not match the original scenario hash, so it makes no sense
269  // to test them, since they always would appear as remote scenarios
270  if(!reloaded) {
271  if(auto hashes = game_config.optional_child("multiplayer_hashes")) {
272  std::string hash = game["hash"];
273  bool hash_found = false;
274  for(const auto & i : hashes->attribute_range()) {
275  if(i.first == game["mp_scenario"] && i.second == hash) {
276  hash_found = true;
277  break;
278  }
279  }
280 
281  if(!hash_found) {
282  remote_scenario = true;
283  info_stream << spaced_em_dash();
284  info_stream << _("Remote scenario");
285  verified = false;
286  }
287  }
288  }
289 
290  if(require) {
291  addon_req result = check_addon_version_compatibility((*level_cfg), game);
292  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
293  }
294  } else {
295  if(require) {
296  addons_outcome = std::max(addons_outcome, addon_req::NEED_DOWNLOAD); // Elevate to most severe error level encountered so far
297  }
298  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), true);
299  scenario = game["mp_scenario_name"].str();
300  info_stream << scenario;
301  verified = false;
302  }
303  } else if(!game["mp_campaign"].empty()) {
304  if(auto campaign_cfg = game_config.find_child("campaign", "id", game["mp_campaign"])) {
305  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), false);
306 
307  std::stringstream campaign_text;
308  campaign_text
309  << campaign_cfg["name"] << spaced_em_dash()
310  << game["mp_scenario_name"];
311 
312  // Difficulty
313  config difficulties = gui2::dialogs::generate_difficulty_config(*campaign_cfg);
314  for(const config& difficulty : difficulties.child_range("difficulty")) {
315  if(difficulty["define"] == game["difficulty_define"]) {
316  campaign_text << spaced_em_dash() << difficulty["description"];
317 
318  break;
319  }
320  }
321 
322  scenario = campaign_text.str();
323  info_stream << campaign_text.rdbuf();
324 
325  // TODO: should we have this?
326  //if(game["require_scenario"].to_bool(false)) {
327  addon_req result = check_addon_version_compatibility(*campaign_cfg, game);
328  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
329  //}
330  } else {
331  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), true);
332  scenario = game["mp_campaign_name"].str();
333  info_stream << scenario;
334  verified = false;
335  }
336  } else {
337  scenario = _("Unknown scenario");
338  info_stream << scenario;
339  verified = false;
340  }
341 
342  // Remove any newlines that might have been in game names (the player-set ones)
343  // No idea how this could happen, but I've seen it (vultraz, 2020-10-26)
344  boost::erase_all(name, "\n");
345 
346  // Remove any newlines that might have been in game titles (scenario/campaign name, etc.)
347  boost::replace_all(scenario, "\n", " " + font::unicode_em_dash + " ");
348 
349  if(reloaded) {
350  info_stream << spaced_em_dash();
351  info_stream << _("Reloaded game");
352  verified = false;
353  }
354 
355  // These should always be present in the data the server sends, but may or may not be empty.
356  // I'm just using child_or_empty here to preempt any cases where they might not be included.
357  const config& s = game.child_or_empty("slot_data");
358  const config& t = game.child_or_empty("turn_data");
359 
360  if(!s.empty()) {
361  started = false;
362 
363  vacant_slots = s["vacant"].to_unsigned();
364 
365  if(vacant_slots > 0) {
366  status = formatter() << _n("Vacant Slot:", "Vacant Slots:", vacant_slots) << " " << vacant_slots << "/" << s["max"];
367  } else {
368  status = _("mp_game_available_slots^Full");
369  }
370  }
371 
372  if(!t.empty()) {
373  started = true;
374 
375  current_turn = t["current"].to_unsigned();
376  const int max_turns = t["max"].to_int();
377 
378  if(max_turns > -1) {
379  status = formatter() << _("Turn") << " " << t["current"] << "/" << max_turns;
380  } else {
381  status = formatter() << _("Turn") << " " << t["current"];
382  }
383  }
384 
385  if(fog) {
386  vision = _("Fog");
387  if(shroud) {
388  vision += "/";
389  vision += _("Shroud");
390  }
391  } else if(shroud) {
392  vision = _("Shroud");
393  } else {
394  vision = _("vision^none");
395  }
396 
397  if(game["mp_countdown"].to_bool()) {
399  << game["mp_countdown_init_time"].str() << "+"
400  << game["mp_countdown_turn_bonus"].str() << "/"
401  << game["mp_countdown_action_bonus"].str();
402  } else {
403  time_limit = _("time limit^none");
404  }
405 
406  map_info = info_stream.str();
407 }
408 
410 {
411  if(!local_item.has_attribute("addon_id") || !local_item.has_attribute("addon_version")) {
412  return addon_req::SATISFIED;
413  }
414 
415  if(auto game_req = game.find_child("addon", "id", local_item["addon_id"])) {
416  if(!game_req["require"].to_bool(false)) {
417  return addon_req::SATISFIED;
418  }
419 
420  required_addon r{local_item["addon_id"].str(), addon_req::SATISFIED, ""};
421 
422  // Local version
423  const version_info local_ver(local_item["addon_version"].str());
424  version_info local_min_ver(local_item.has_attribute("addon_min_version") ? local_item["addon_min_version"] : local_item["addon_version"]);
425 
426  // If the UMC didn't specify last compatible version, assume no backwards compatibility.
427  // Also apply some sanity checking regarding min version; if the min ver doesn't make sense, ignore it.
428  local_min_ver = std::min(local_min_ver, local_ver);
429 
430  // Remote version
431  const version_info remote_ver(game_req["version"].str());
432  version_info remote_min_ver(game_req->has_attribute("min_version") ? game_req["min_version"] : game_req["version"]);
433 
434  remote_min_ver = std::min(remote_min_ver, remote_ver);
435 
436  // Check if the host is too out of date to play.
437  if(local_min_ver > remote_ver) {
438  DBG_LB << "r.outcome = CANNOT_SATISFY for item='" << local_item["id"]
439  << "' addon='" << local_item["addon_id"]
440  << "' addon_min_version='" << local_item["addon_min_version"]
441  << "' addon_min_version_parsed='" << local_min_ver.str()
442  << "' addon_version='" << local_item["addon_version"]
443  << "' remote_ver='" << remote_ver.str()
444  << "'";
445  r.outcome = addon_req::CANNOT_SATISFY;
446 
447  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>.", {
448  {"addon", local_item["addon_title"].str()},
449  {"host_ver", remote_ver.str()},
450  {"local_ver", local_ver.str()}
451  });
452 
453  required_addons.push_back(r);
454  return r.outcome;
455  }
456 
457  // Check if our version is too out of date to play.
458  if(remote_min_ver > local_ver) {
459  r.outcome = addon_req::NEED_DOWNLOAD;
460 
461  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>.", {
462  {"addon", local_item["addon_title"].str()},
463  {"host_ver", remote_ver.str()},
464  {"local_ver", local_ver.str()}
465  });
466 
467  required_addons.push_back(r);
468  return r.outcome;
469  }
470  }
471 
472  return addon_req::SATISFIED;
473 }
474 
476 {
477  return !started && vacant_slots > 0;
478 }
479 
481 {
483 }
484 
486 {
487  switch(display_status) {
489  return "clean";
491  return "new";
493  return "deleted";
495  return "updated";
496  default:
497  ERR_CF << "BAD display_status " << static_cast<int>(display_status) << " in game " << id;
498  return "?";
499  }
500 }
501 
502 bool game_info::match_string_filter(const std::string& filter) const
503 {
504  const std::string& s1 = name;
505  const std::string& s2 = map_info;
506  return std::search(s1.begin(), s1.end(), filter.begin(), filter.end(),
507  utils::chars_equal_insensitive) != s1.end()
508  || std::search(s2.begin(), s2.end(), filter.begin(), filter.end(),
509  utils::chars_equal_insensitive) != s2.end();
510 }
511 
512 }
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:191
double t
Definition: astarsearch.cpp:65
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:161
bool has_attribute(config_key_type key) const
Definition: config.cpp:159
child_itors child_range(config_key_type key)
Definition: config.cpp:277
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
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:215
New lexcical_cast header.
static lg::log_domain log_engine("engine")
static lg::log_domain log_lobby("lobby")
#define DBG_LB
Definition: lobby_data.cpp:48
#define ERR_CF
Definition: lobby_data.cpp:43
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:63
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
Main entry points of multiplayer mode.
Definition: lobby_data.cpp:52
bool logged_in_as_moderator()
Gets whether the currently logged-in user is a moderator.
bool fog()
Definition: game.cpp:525
bool shroud()
Definition: game.cpp:535
bool is_ignored(const std::string &nick)
Definition: game.cpp:276
std::string era()
Definition: game.cpp:681
bool shuffle_sides()
Definition: game.cpp:461
bool use_map_settings()
Definition: game.cpp:481
std::string login()
bool is_friend(const std::string &nick)
Definition: game.cpp:264
int icompare(const std::string &s1, const std::string &s2)
Case-insensitive lexicographical comparison.
Definition: gettext.cpp:521
bool chars_equal_insensitive(char a, char b)
Definition: general.hpp:24
std::string name
Definition: lobby_data.hpp:74
std::string type_marker
Definition: lobby_data.hpp:76
bool can_join() const
Definition: lobby_data.cpp:475
std::vector< required_addon > required_addons
Definition: lobby_data.hpp:128
std::string scenario
Definition: lobby_data.hpp:75
std::string era
Definition: lobby_data.hpp:80
std::string map_size_info
Definition: lobby_data.hpp:79
addon_req check_addon_version_compatibility(const config &local_item, const config &game)
Definition: lobby_data.cpp:409
unsigned int current_turn
Definition: lobby_data.hpp:93
bool remote_scenario
Definition: lobby_data.hpp:77
std::string map_info
Definition: lobby_data.hpp:78
std::string status
Definition: lobby_data.hpp:89
addon_req addons_outcome
Definition: lobby_data.hpp:129
std::size_t vacant_slots
Definition: lobby_data.hpp:91
game_info(const config &c, const std::vector< std::string > &installed_addons)
Definition: lobby_data.cpp:114
disp_status display_status
Definition: lobby_data.hpp:118
bool can_observe() const
Definition: lobby_data.cpp:480
std::string vision
Definition: lobby_data.hpp:88
std::string time_limit
Definition: lobby_data.hpp:90
const char * display_status_string() const
Definition: lobby_data.cpp:485
std::vector< std::pair< std::string, bool > > mod_info
List of modification names and whether they're installed or not.
Definition: lobby_data.hpp:83
bool match_string_filter(const std::string &filter) const
Definition: lobby_data.cpp:502
std::string map_data
Definition: lobby_data.hpp:73
This class represents the information a client has about another player.
Definition: lobby_data.hpp:33
user_state get_state(int selected_game_id) const
Definition: lobby_data.cpp:64
user_relation get_relation() const
Definition: lobby_data.cpp:75
std::string name
Definition: lobby_data.hpp:54
bool operator<(const user_info &b) const
Definition: lobby_data.cpp:88
user_info(const config &c)
Definition: lobby_data.cpp:54
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