The Battle for Wesnoth  1.15.12+dev
lobby_data.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2018 by Tomasz Sniatowski <kailoran@gmail.com>
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
16 
17 #include "config.hpp"
18 #include "filesystem.hpp"
19 #include "font/pango/escape.hpp"
20 #include "formatter.hpp"
21 #include "formula/string_utils.hpp"
22 #include "game_config_manager.hpp"
23 #include "game_config_view.hpp"
25 #include "game_version.hpp"
26 #include "gettext.hpp"
28 #include "lexical_cast.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 #include <iterator>
38 
39 #include <boost/algorithm/string.hpp>
40 
41 static lg::log_domain log_config("config");
42 #define ERR_CF LOG_STREAM(err, log_config)
43 static lg::log_domain log_engine("engine");
44 #define WRN_NG LOG_STREAM(warn, log_engine)
45 
46 static lg::log_domain log_lobby("lobby");
47 #define DBG_LB LOG_STREAM(info, log_lobby)
48 #define LOG_LB LOG_STREAM(info, log_lobby)
49 #define ERR_LB LOG_STREAM(err, log_lobby)
50 
51 namespace mp {
52 
54  const std::string& user,
55  const std::string& message)
56  : timestamp(timestamp), user(user), message(message)
57 {
58 }
59 
61 {
62 }
63 
64 void chat_session::add_message(const std::time_t& timestamp,
65  const std::string& user,
66  const std::string& message)
67 {
68  history_.emplace_back(timestamp, user, message);
69 }
70 
71 
72 void chat_session::add_message(const std::string& user, const std::string& message)
73 {
74  add_message(std::time(nullptr), user, message);
75 }
76 
78 {
79  history_.clear();
80 }
81 
82 room_info::room_info(const std::string& name) : name_(name), members_(), log_()
83 {
84 }
85 
86 bool room_info::is_member(const std::string& user) const
87 {
88  return members_.find(user) != members_.end();
89 }
90 
91 void room_info::add_member(const std::string& user)
92 {
93  members_.insert(user);
94 }
95 
96 void room_info::remove_member(const std::string& user)
97 {
98  members_.erase(user);
99 }
100 
102 {
103  members_.clear();
104  for(const auto & m : data.child_range("member"))
105  {
106  members_.insert(m["name"]);
107  }
108 }
109 
111  : name(c["name"])
112  , forum_id(c["forum_id"].to_int())
113  , game_id(c["game_id"])
114  , relation(user_relation::ME)
115  , state(game_id == 0 ? user_state::LOBBY : user_state::GAME)
116  , registered(c["registered"].to_bool())
117  , observing(c["status"] == "observing")
118  , moderator(c["moderator"].to_bool(false))
119 {
120  update_relation();
121 }
122 
123 void user_info::update_state(int selected_game_id,
124  const room_info* current_room /*= nullptr*/)
125 {
126  if(game_id != 0) {
127  if(game_id == selected_game_id) {
129  } else {
131  }
132  } else {
133  if(current_room != nullptr && current_room->is_member(name)) {
135  } else {
137  }
138  }
139  update_relation();
140 }
141 
143 {
144  if(name == preferences::login()) {
146  } else if(preferences::is_ignored(name)) {
148  } else if(preferences::is_friend(name)) {
150  } else {
152  }
153 }
154 
155 bool user_info::operator<(const user_info& b) const
156 {
157  return relation < b.relation || (relation == b.relation && translation::icompare(name, b.name) < 0);
158 }
159 
160 namespace
161 {
162 const std::string& spaced_em_dash()
163 {
164  static const std::string res = " " + font::unicode_em_dash + " ";
165  return res;
166 }
167 
168 std::string make_game_type_marker(const std::string& text, bool color_for_missing)
169 {
170  if(color_for_missing) {
171  return formatter() << "<b><span color='#f00'>[" << text << "]</span></b> ";
172  } else {
173  return formatter() << "<b>[" << text << "]</b> ";
174  }
175 }
176 
177 } // end anon namespace
178 
179 game_info::game_info(const config& game, const std::vector<std::string>& installed_addons)
180  : id(game["id"])
181  , map_data(game["map_data"])
182  , name(font::escape_text(game["name"]))
183  , scenario()
184  , type_marker()
185  , remote_scenario(false)
186  , map_info()
187  , map_size_info()
188  , era()
189  , gold(game["mp_village_gold"])
190  , support(game["mp_village_support"])
191  , xp(game["experience_modifier"].str() + "%")
192  , vision()
193  , status()
194  , time_limit()
195  , vacant_slots()
196  , current_turn(0)
197  , reloaded(game["savegame"].to_enum<mp_game_settings::SAVED_GAME_MODE>(mp_game_settings::SAVED_GAME_MODE::NONE) != mp_game_settings::SAVED_GAME_MODE::NONE)
198  , started(false)
199  , fog(game["mp_fog"].to_bool())
200  , shroud(game["mp_shroud"].to_bool())
201  , observers(game["observer"].to_bool(true))
202  , shuffle_sides(game["shuffle_sides"].to_bool(true))
203  , use_map_settings(game["mp_use_map_settings"].to_bool())
204  , private_replay(game["private_replay"].to_bool())
205  , verified(true)
206  , password_required(game["password"].to_bool())
207  , have_era(true)
208  , have_all_mods(true)
209  , has_friends(false)
210  , has_ignored(false)
211  , display_status(disp_status::NEW)
212  , required_addons()
213  , addons_outcome(addon_req::SATISFIED)
214 {
216 
217  // Parse the list of addons required to join this game.
218  for(const config& addon : game.child_range("addon")) {
219  if(addon.has_attribute("id") && addon["require"].to_bool(false)) {
220  if(std::find(installed_addons.begin(), installed_addons.end(), addon["id"].str()) == installed_addons.end()) {
221  required_addon r;
222  r.addon_id = addon["id"].str();
224 
225  // Use addon name if provided, else fall back on the addon id.
226  if(addon.has_attribute("name")) {
227  r.message = VGETTEXT("Missing addon: $name", {{"name", addon["name"].str()}});
228  } else {
229  r.message = VGETTEXT("Missing addon: $id", {{"id", addon["id"].str()}});
230  }
231 
232  required_addons.push_back(std::move(r));
233 
236  }
237  }
238  }
239  }
240 
241  if(!game["mp_era"].empty()) {
242  const config& era_cfg = game_config.find_child("era", "id", game["mp_era"]);
243  const bool require = game["require_era"].to_bool(true);
244  if(era_cfg) {
245  era = era_cfg["name"].str();
246 
247  if(require) {
248  addon_req result = check_addon_version_compatibility(era_cfg, game);
249  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
250  }
251  } else {
252  have_era = !require;
253  era = game["mp_era_name"].str();
254  verified = false;
255 
256  if(!have_era) {
258  }
259  }
260  } else {
261  era = _("Unknown era");
262  verified = false;
263  }
264 
265  std::stringstream info_stream;
266  info_stream << era;
267 
268  for(const config& cfg : game.child_range("modification")) {
269  mod_info.emplace_back(cfg["name"].str(), true);
270  info_stream << ' ' << mod_info.back().first;
271 
272  if(cfg["require_modification"].to_bool(false)) {
273  if(const config& mod = game_config.find_child("modification", "id", cfg["id"])) {
274  addon_req result = check_addon_version_compatibility(mod, game);
275  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
276  } else {
277  have_all_mods = false;
278  mod_info.back().second = false;
279 
281  }
282  }
283  }
284 
285  std::sort(mod_info.begin(), mod_info.end(), [](const auto& lhs, const auto& rhs) {
286  return translation::icompare(lhs.first, rhs.first) < 0;
287  });
288 
289  info_stream << ' ';
290 
291  if(map_data.empty()) {
292  map_data = filesystem::read_map(game["mp_scenario"]);
293  }
294 
295  if(map_data.empty()) {
296  info_stream << " — ??×??";
297  } else {
298  try {
299  gamemap map(map_data);
300  std::ostringstream msi;
301  msi << map.w() << font::unicode_multiplication_sign << map.h();
302  map_size_info = msi.str();
303  info_stream << spaced_em_dash() << map_size_info;
304  } catch(const incorrect_map_format_error&) {
305  verified = false;
306  } catch(const wml_exception& e) {
307  ERR_CF << "map could not be loaded: " << e.dev_message << '\n';
308  verified = false;
309  }
310  }
311 
312  info_stream << " ";
313 
314  //
315  // Check scenarios and campaigns
316  //
317  if(!game["mp_scenario"].empty() && game["mp_campaign"].empty()) {
318  // Check if it's a multiplayer scenario
319  const config* level_cfg = &game_config.find_child("multiplayer", "id", game["mp_scenario"]);
320  const bool require = game["require_scenario"].to_bool(false);
321 
322  // Check if it's a user map
323  if(!*level_cfg) {
324  level_cfg = &game_config.find_child("generic_multiplayer", "id", game["mp_scenario"]);
325  }
326 
327  if(*level_cfg) {
328  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), false);
329  scenario = (*level_cfg)["name"].str();
330  info_stream << scenario;
331 
332  // Reloaded games do not match the original scenario hash, so it makes no sense
333  // to test them, since they always would appear as remote scenarios
334  if(!reloaded) {
335  if(const config& hashes = game_config.child("multiplayer_hashes")) {
336  std::string hash = game["hash"];
337  bool hash_found = false;
338  for(const auto & i : hashes.attribute_range()) {
339  if(i.first == game["mp_scenario"] && i.second == hash) {
340  hash_found = true;
341  break;
342  }
343  }
344 
345  if(!hash_found) {
346  remote_scenario = true;
347  info_stream << spaced_em_dash();
348  info_stream << _("Remote scenario");
349  verified = false;
350  }
351  }
352  }
353 
354  if(require) {
355  addon_req result = check_addon_version_compatibility((*level_cfg), game);
356  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
357  }
358  } else {
359  if(require) {
360  addons_outcome = std::max(addons_outcome, addon_req::NEED_DOWNLOAD); // Elevate to most severe error level encountered so far
361  }
362  type_marker = make_game_type_marker(_("scenario_abbreviation^S"), true);
363  scenario = game["mp_scenario_name"].str();
364  info_stream << scenario;
365  verified = false;
366  }
367  } else if(!game["mp_campaign"].empty()) {
368  if(const config& campaign_cfg = game_config.find_child("campaign", "id", game["mp_campaign"])) {
369  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), false);
370 
371  std::stringstream campaign_text;
372  campaign_text
373  << campaign_cfg["name"] << spaced_em_dash()
374  << game["mp_scenario_name"];
375 
376  // Difficulty
377  config difficulties = gui2::dialogs::generate_difficulty_config(campaign_cfg);
378  for(const config& difficulty : difficulties.child_range("difficulty")) {
379  if(difficulty["define"] == game["difficulty_define"]) {
380  campaign_text << spaced_em_dash() << difficulty["description"];
381 
382  break;
383  }
384  }
385 
386  scenario = campaign_text.str();
387  info_stream << campaign_text.rdbuf();
388 
389  // TODO: should we have this?
390  //if(game["require_scenario"].to_bool(false)) {
391  addon_req result = check_addon_version_compatibility(campaign_cfg, game);
392  addons_outcome = std::max(addons_outcome, result); // Elevate to most severe error level encountered so far
393  //}
394  } else {
395  type_marker = make_game_type_marker(_("campaign_abbreviation^C"), true);
396  scenario = game["mp_campaign_name"].str();
397  info_stream << scenario;
398  verified = false;
399  }
400  } else {
401  scenario = _("Unknown scenario");
402  info_stream << scenario;
403  verified = false;
404  }
405 
406  // Remove any newlines that might have been in game names (the player-set ones)
407  // No idea how this could happen, but I've seen it (vultraz, 2020-10-26)
408  boost::erase_all(name, "\n");
409 
410  // Remove any newlines that might have been in game titles (scenario/campaign name, etc.)
411  boost::replace_all(scenario, "\n", " " + font::unicode_em_dash + " ");
412 
413  if(reloaded) {
414  info_stream << spaced_em_dash();
415  info_stream << _("Reloaded game");
416  verified = false;
417  }
418 
419  // These should always be present in the data the server sends, but may or may not be empty.
420  // I'm just using child_or_empty here to preempt any cases where they might not be included.
421  const config& s = game.child_or_empty("slot_data");
422  const config& t = game.child_or_empty("turn_data");
423 
424  if(!s.empty()) {
425  started = false;
426 
427  vacant_slots = s["vacant"].to_unsigned();
428 
429  if(vacant_slots > 0) {
430  status = formatter() << _n("Vacant Slot:", "Vacant Slots:", vacant_slots) << " " << vacant_slots << "/" << s["max"];
431  } else {
432  status = _("mp_game_available_slots^Full");
433  }
434  }
435 
436  if(!t.empty()) {
437  started = true;
438 
439  current_turn = t["current"].to_unsigned();
440  const int max_turns = t["max"].to_int();
441 
442  if(max_turns > -1) {
443  status = formatter() << _("Turn") << " " << t["current"] << "/" << max_turns;
444  } else {
445  status = formatter() << _("Turn") << " " << t["current"];
446  }
447  }
448 
449  if(fog) {
450  vision = _("Fog");
451  if(shroud) {
452  vision += "/";
453  vision += _("Shroud");
454  }
455  } else if(shroud) {
456  vision = _("Shroud");
457  } else {
458  vision = _("vision^none");
459  }
460 
461  if(game["mp_countdown"].to_bool()) {
463  << game["mp_countdown_init_time"].str() << "+"
464  << game["mp_countdown_turn_bonus"].str() << "/"
465  << game["mp_countdown_action_bonus"].str();
466  } else {
467  time_limit = _("time limit^none");
468  }
469 
470  map_info = info_stream.str();
471 }
472 
474 {
475  if(!local_item.has_attribute("addon_id") || !local_item.has_attribute("addon_version")) {
476  return addon_req::SATISFIED;
477  }
478 
479  if(const config& game_req = game.find_child("addon", "id", local_item["addon_id"])) {
480  if(!game_req["require"].to_bool(false)) {
481  return addon_req::SATISFIED;
482  }
483 
484  required_addon r{local_item["addon_id"].str(), addon_req::SATISFIED, ""};
485 
486  // Local version
487  const version_info local_ver(local_item["addon_version"].str());
488  version_info local_min_ver(local_item.has_attribute("addon_min_version") ? local_item["addon_min_version"] : local_item["addon_version"]);
489 
490  // If the UMC didn't specify last compatible version, assume no backwards compatibility.
491  // Also apply some sanity checking regarding min version; if the min ver doesn't make sense, ignore it.
492  local_min_ver = std::min(local_min_ver, local_ver);
493 
494  // Remote version
495  const version_info remote_ver(game_req["version"].str());
496  version_info remote_min_ver(game_req.has_attribute("min_version") ? game_req["min_version"] : game_req["version"]);
497 
498  remote_min_ver = std::min(remote_min_ver, remote_ver);
499 
500  // Check if the host is too out of date to play.
501  if(local_min_ver > remote_ver) {
502  DBG_LB << "r.outcome = CANNOT_SATISFY for item='" << local_item["id"]
503  << "' addon='" << local_item["addon_id"]
504  << "' addon_min_version='" << local_item["addon_min_version"]
505  << "' addon_min_version_parsed='" << local_min_ver.str()
506  << "' addon_version='" << local_item["addon_version"]
507  << "' remote_ver='" << remote_ver.str()
508  << "'\n";
509  r.outcome = addon_req::CANNOT_SATISFY;
510 
511  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>.", {
512  {"addon", local_item["addon_title"].str()},
513  {"host_ver", remote_ver.str()},
514  {"local_ver", local_ver.str()}
515  });
516 
517  required_addons.push_back(r);
518  return r.outcome;
519  }
520 
521  // Check if our version is too out of date to play.
522  if(remote_min_ver > local_ver) {
523  r.outcome = addon_req::NEED_DOWNLOAD;
524 
525  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>.", {
526  {"addon", local_item["addon_title"].str()},
527  {"host_ver", remote_ver.str()},
528  {"local_ver", local_ver.str()}
529  });
530 
531  required_addons.push_back(r);
532  return r.outcome;
533  }
534  }
535 
536  return addon_req::SATISFIED;
537 }
538 
540 {
541  return !started && vacant_slots > 0;
542 }
543 
545 {
547 }
548 
550 {
551  switch(display_status) {
553  return "clean";
555  return "new";
557  return "deleted";
559  return "updated";
560  default:
561  ERR_CF << "BAD display_status " << static_cast<int>(display_status) << " in game " << id << "\n";
562  return "?";
563  }
564 }
565 
566 bool game_info::match_string_filter(const std::string& filter) const
567 {
568  const std::string& s1 = name;
569  const std::string& s2 = map_info;
570  return std::search(s1.begin(), s1.end(), filter.begin(), filter.end(),
571  utils::chars_equal_insensitive) != s1.end()
572  || std::search(s2.begin(), s2.end(), filter.begin(), filter.end(),
573  utils::chars_equal_insensitive) != s2.end();
574 }
575 
576 }
std::string scenario
Definition: lobby_data.hpp:151
std::string map_size_info
Definition: lobby_data.hpp:155
static std::string _n(const char *str1, const char *str2, int n)
Definition: gettext.hpp:96
void add_message(const std::time_t &timestamp, const std::string &user, const std::string &message)
Definition: lobby_data.cpp:64
std::string status
Definition: lobby_data.hpp:165
std::string era()
Definition: game.cpp:689
#define ERR_CF
Definition: lobby_data.cpp:42
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:159
Collection of helper functions relating to Pango formatting.
unsigned int current_turn
Definition: lobby_data.hpp:169
std::deque< chat_message > history_
Definition: lobby_data.hpp:61
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:860
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:207
user_state state
Definition: lobby_data.hpp:132
addon_req addons_outcome
Definition: lobby_data.hpp:204
child_itors child_range(config_key_type key)
Definition: config.cpp:356
bool shuffle_sides()
Definition: game.cpp:469
std::set< std::string > members_
Definition: lobby_data.hpp:96
bool chars_equal_insensitive(char a, char b)
Definition: general.hpp:22
std::string map_info
Definition: lobby_data.hpp:154
bool remote_scenario
Definition: lobby_data.hpp:153
static std::string _(const char *str)
Definition: gettext.hpp:92
Definitions for the interface to Wesnoth Markup Language (WML).
std::string name
Definition: lobby_data.hpp:150
std::string time_limit
Definition: lobby_data.hpp:166
#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:533
Main entry points of multiplayer mode.
Definition: lobby_data.cpp:51
static game_config_manager * get()
user_info(const config &c)
Definition: lobby_data.cpp:110
chat_message(const std::time_t &timestamp, const std::string &user, const std::string &message)
Create a chat message.
Definition: lobby_data.cpp:53
bool is_friend(const std::string &nick)
Definition: game.cpp:272
int w() const
Effective map width.
Definition: map.hpp:49
std::ostringstream wrapper.
Definition: formatter.hpp:38
#define DBG_LB
Definition: lobby_data.cpp:47
const game_config_view & game_config() const
std::string vision
Definition: lobby_data.hpp:164
const std::string unicode_multiplication_sign
Definition: constants.cpp:45
Encapsulates the map of the game.
Definition: map.hpp:170
static lg::log_domain log_config("config")
disp_status display_status
Definition: lobby_data.hpp:193
std::string map_data
Definition: lobby_data.hpp:149
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:177
bool is_member(const std::string &user) const
Definition: lobby_data.cpp:86
bool is_ignored(const std::string &nick)
Definition: game.cpp:284
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:128
std::string dev_message
The message for developers telling which problem was triggered, this shouldn&#39;t be translated...
bool shroud()
Definition: game.cpp:543
std::string escape_text(const std::string &text)
Escapes the pango markup characters in a text.
Definition: escape.hpp:32
const char * display_status_string() const
Definition: lobby_data.cpp:549
std::size_t i
Definition: function.cpp:940
static lg::log_domain log_engine("engine")
Default, unset return value.
Definition: retval.hpp:31
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
Game configuration data as global variables.
Definition: build_info.cpp:58
static map_location::DIRECTION s
std::string type_marker
Definition: lobby_data.hpp:152
bool use_map_settings()
Definition: game.cpp:489
This class represents the information a client has about a room.
Definition: lobby_data.hpp:67
addon_req check_addon_version_compatibility(const config &local_item, const config &game)
Definition: lobby_data.cpp:473
Declarations for File-IO.
bool can_observe() const
Definition: lobby_data.cpp:544
static int sort(lua_State *L)
Definition: ltablib.cpp:397
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
Represents version numbers.
std::string era
Definition: lobby_data.hpp:156
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:104
void update_state(int selected_game_id, const room_info *current_room=nullptr)
Definition: lobby_data.cpp:123
room_info(const std::string &name)
Definition: lobby_data.cpp:82
double t
Definition: astarsearch.cpp:64
static bool timestamp
Definition: log.cpp:43
int icompare(const std::string &s1, const std::string &s2)
Case-insensitive lexicographical comparison.
Definition: gettext.cpp:474
std::size_t vacant_slots
Definition: lobby_data.hpp:167
const std::string unicode_em_dash
Definition: constants.cpp:43
void remove_member(const std::string &user)
Definition: lobby_data.cpp:96
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:203
void update_relation()
Definition: lobby_data.cpp:142
#define e
game_info(const config &c, const std::vector< std::string > &installed_addons)
Definition: lobby_data.cpp:179
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:477
user_relation relation
Definition: lobby_data.hpp:131
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:59
bool can_join() const
Definition: lobby_data.cpp:539
mock_char c
int h() const
Effective map height.
Definition: map.hpp:52
bool operator<(const user_info &b) const
Definition: lobby_data.cpp:155
bool empty() const
Definition: config.cpp:916
const config & child(config_key_type key) const
void process_room_members(const config &data)
Definition: lobby_data.cpp:101
bool match_string_filter(const std::string &filter) const
Definition: lobby_data.cpp:566
const std::string & name() const
Definition: lobby_data.hpp:72
void add_member(const std::string &user)
Definition: lobby_data.cpp:91