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