The Battle for Wesnoth  1.19.16+dev
lobby_data.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2025
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"
28 #include "log.hpp"
29 #include "map/exception.hpp"
30 #include "map/map.hpp"
31 #include "mp_game_settings.hpp"
33 #include "serialization/markup.hpp"
34 #include "utils/general.hpp"
35 #include "wml_exception.hpp"
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"].to_int())
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 markup::span_color("#f00", markup::bold("[", text, "] "));
104  } else {
105  return markup::bold("[", text, "] ");
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"].to_int())
113  , map_data(game["map_data"])
114  , name(font::escape_text(game["name"].str()))
115  , scenario()
116  , scenario_id()
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  , game_preset(game["game_preset"].to_bool())
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["required"].to_bool(false)) {
155  if(!utils::contains(installed_addons, addon["id"].str())) {
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  auto 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) {
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(true)) {
208  if(auto mod = game_config.find_child("modification", "id", cfg["id"])) {
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;
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"]).ptr();
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"]).ptr();
260  }
261 
262  if(level_cfg) {
263  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), false);
264  scenario = (*level_cfg)["name"].str();
265  scenario_id = (*level_cfg)["id"].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 scenario = game["mp_scenario"];
273  if(hashes[scenario] != game["hash"]) {
274  remote_scenario = true;
275  info_stream << spaced_em_dash();
276  info_stream << _("Remote scenario");
277  verified = false;
278  }
279  }
280  }
281 
282  if(require) {
283  addon_req result = check_addon_version_compatibility((*level_cfg), game);
284  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
285  }
286  } else {
287  if(require) {
288  addons_outcome = std::max(addons_outcome, addon_req::NEED_DOWNLOAD); // Elevate to most severe error level encountered so far
289  }
290  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), true);
291  scenario = game["mp_scenario_name"].str();
292  info_stream << scenario;
293  verified = false;
294  }
295  } else if(!game["mp_campaign"].empty()) {
296  if(auto campaign_cfg = game_config.find_child("campaign", "id", game["mp_campaign"])) {
297  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), false);
298 
299  std::stringstream campaign_text;
300  campaign_text
301  << campaign_cfg["name"] << spaced_em_dash()
302  << game["mp_scenario_name"];
303 
304  // Difficulty
305  for(const config& difficulty : campaign_cfg->child_range("difficulty")) {
306  if(difficulty["define"] == game["difficulty_define"]) {
307  campaign_text << spaced_em_dash() << difficulty["description"];
308  break;
309  }
310  }
311 
312  scenario = campaign_text.str();
313  info_stream << campaign_text.rdbuf();
314 
315  // TODO: should we have this?
316  //if(game["require_scenario"].to_bool(false)) {
317  addon_req result = check_addon_version_compatibility(*campaign_cfg, game);
318  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
319  //}
320  } else {
321  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), true);
322  scenario = game["mp_campaign_name"].str();
323  info_stream << scenario;
324  verified = false;
325  }
326  } else {
327  scenario = _("Unknown scenario");
328  info_stream << scenario;
329  verified = false;
330  }
331 
332  // Remove any newlines that might have been in game names (the player-set ones)
333  // No idea how this could happen, but I've seen it (vultraz, 2020-10-26)
334  boost::erase_all(name, "\n");
335 
336  // Remove any newlines that might have been in game titles (scenario/campaign name, etc.)
337  boost::replace_all(scenario, "\n", " " + font::unicode_em_dash + " ");
338 
339  if(reloaded) {
340  info_stream << spaced_em_dash();
341  info_stream << _("Reloaded game");
342  verified = false;
343  }
344 
345  // These should always be present in the data the server sends, but may or may not be empty.
346  // I'm just using child_or_empty here to preempt any cases where they might not be included.
347  const config& s = game.child_or_empty("slot_data");
348  const config& t = game.child_or_empty("turn_data");
349 
350  if(!s.empty()) {
351  started = false;
352 
353  vacant_slots = s["vacant"].to_unsigned();
354 
355  if(vacant_slots > 0) {
356  status = formatter() << _n("Vacant Slot:", "Vacant Slots:", vacant_slots) << " " << vacant_slots << "/" << s["max"];
357  } else {
358  status = _("mp_game_available_slots^Full");
359  }
360  }
361 
362  if(!t.empty()) {
363  started = true;
364 
365  current_turn = t["current"].to_unsigned();
366  const int max_turns = t["max"].to_int();
367 
368  if(max_turns > -1) {
369  status = formatter() << _("Turn") << " " << t["current"] << "/" << max_turns;
370  } else {
371  status = formatter() << _("Turn") << " " << t["current"];
372  }
373  }
374 
375  if(fog) {
376  vision = _("Fog");
377  if(shroud) {
378  vision += "/";
379  vision += _("Shroud");
380  }
381  } else if(shroud) {
382  vision = _("Shroud");
383  } else {
384  vision = _("vision^none");
385  }
386 
387  if(game["mp_countdown"].to_bool()) {
389  << game["mp_countdown_init_time"].str() << "+"
390  << game["mp_countdown_turn_bonus"].str() << "/"
391  << game["mp_countdown_action_bonus"].str();
392  } else {
393  time_limit = _("time limit^none");
394  }
395 
396  map_info = info_stream.str();
397 }
398 
400 {
401  if(!local_item.has_attribute("addon_id") || !local_item.has_attribute("addon_version")) {
402  return addon_req::SATISFIED;
403  }
404 
405  if(auto game_req = game.find_child("addon", "id", local_item["addon_id"])) {
406  if(!game_req["required"].to_bool(false)) {
407  return addon_req::SATISFIED;
408  }
409 
410  required_addon r{local_item["addon_id"].str(), addon_req::SATISFIED, ""};
411 
412  // Local version
413  const version_info local_ver(local_item["addon_version"].str());
414  version_info local_min_ver(local_item.has_attribute("addon_min_version") ? local_item["addon_min_version"] : local_item["addon_version"]);
415 
416  // If the UMC didn't specify last compatible version, assume no backwards compatibility.
417  // Also apply some sanity checking regarding min version; if the min ver doesn't make sense, ignore it.
418  local_min_ver = std::min(local_min_ver, local_ver);
419 
420  // Remote version
421  const version_info remote_ver(game_req["version"].str());
422  version_info remote_min_ver(game_req->has_attribute("min_version") ? game_req["min_version"] : game_req["version"]);
423 
424  remote_min_ver = std::min(remote_min_ver, remote_ver);
425 
426  // Check if the host is too out of date to play.
427  if(local_min_ver > remote_ver) {
428  DBG_LB << "r.outcome = CANNOT_SATISFY for item='" << local_item["id"]
429  << "' addon='" << local_item["addon_id"]
430  << "' addon_min_version='" << local_item["addon_min_version"]
431  << "' addon_min_version_parsed='" << local_min_ver.str()
432  << "' addon_version='" << local_item["addon_version"]
433  << "' remote_ver='" << remote_ver.str()
434  << "'";
435  r.outcome = addon_req::CANNOT_SATISFY;
436 
437  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>.", {
438  {"addon", local_item["addon_title"].str()},
439  {"host_ver", remote_ver.str()},
440  {"local_ver", local_ver.str()}
441  });
442 
443  required_addons.push_back(r);
444  return r.outcome;
445  }
446 
447  // Check if our version is too out of date to play.
448  if(remote_min_ver > local_ver) {
449  r.outcome = addon_req::NEED_DOWNLOAD;
450 
451  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>.", {
452  {"addon", local_item["addon_title"].str()},
453  {"host_ver", remote_ver.str()},
454  {"local_ver", local_ver.str()}
455  });
456 
457  required_addons.push_back(r);
458  return r.outcome;
459  }
460  }
461 
462  return addon_req::SATISFIED;
463 }
464 
466 {
467  return !started && vacant_slots > 0;
468 }
469 
471 {
473 }
474 
476 {
477  switch(display_status) {
479  return "clean";
481  return "new";
483  return "deleted";
485  return "updated";
486  default:
487  ERR_CF << "BAD display_status " << static_cast<int>(display_status) << " in game " << id;
488  return "?";
489  }
490 }
491 
492 bool game_info::match_string_filter(const std::string& filter) const
493 {
494  const std::string& s1 = name;
495  const std::string& s2 = map_info;
497 }
498 
499 }
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:186
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:157
child_itors child_range(std::string_view key)
Definition: config.cpp:267
bool has_attribute(std::string_view key) const
Definition: config.cpp:156
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.
Definitions for the interface to Wesnoth Markup Language (WML).
const config * cfg
Declarations for File-IO.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
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:101
static std::string _(const char *str)
Definition: gettext.hpp:97
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
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)
Graphical text output.
const std::string unicode_em_dash
Definition: constants.cpp:44
const std::string unicode_multiplication_sign
Definition: constants.cpp:46
std::string escape_text(std::string_view text)
Escapes the pango markup characters in a text.
Definition: escape.hpp:33
Game configuration data as global variables.
Definition: build_info.cpp:61
std::string bold(Args &&... data)
Applies bold Pango markup to the input.
Definition: markup.hpp:161
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
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 ci_search(const std::string &s1, const std::string &s2)
Case-insensitive search.
Definition: gettext.cpp:559
constexpr auto filter
Definition: ranges.hpp:38
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:87
std::string name
Definition: lobby_data.hpp:70
std::string scenario_id
Definition: lobby_data.hpp:72
std::string type_marker
Definition: lobby_data.hpp:73
bool can_join() const
Definition: lobby_data.cpp:465
std::vector< required_addon > required_addons
Definition: lobby_data.hpp:127
std::string scenario
Definition: lobby_data.hpp:71
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:399
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:128
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:117
bool can_observe() const
Definition: lobby_data.cpp:470
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:475
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:492
std::string map_data
Definition: lobby_data.hpp:69
This class represents the information a client has about another player.
Definition: lobby_data.hpp:29
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:50
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