The Battle for Wesnoth  1.19.11+dev
lobby.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 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
19 
20 #include "gui/auxiliary/field.hpp"
21 #include "gui/dialogs/message.hpp"
25 
26 #include "gui/core/timer.hpp"
27 #include "gui/widgets/button.hpp"
28 #include "gui/widgets/image.hpp"
29 #include "gui/widgets/label.hpp"
30 #include "gui/widgets/listbox.hpp"
32 #include "gui/widgets/minimap.hpp"
33 #include "gui/widgets/chatbox.hpp"
34 #include "gui/widgets/text_box.hpp"
38 
39 #include "addon/client.hpp"
40 #include "addon/manager_ui.hpp"
41 #include "chat_log.hpp"
42 #include "desktop/open.hpp"
43 #include "serialization/markup.hpp"
44 #include "formatter.hpp"
45 #include "formula/string_utils.hpp"
47 #include "gettext.hpp"
48 #include "help/help.hpp"
49 #include "wesnothd_connection.hpp"
50 
51 #include <functional>
52 
53 static lg::log_domain log_lobby("lobby");
54 #define DBG_LB LOG_STREAM(debug, log_lobby)
55 #define LOG_LB LOG_STREAM(info, log_lobby)
56 #define ERR_LB LOG_STREAM(err, log_lobby)
57 #define SCOPE_LB log_scope2(log_lobby, __func__)
58 
59 namespace gui2::dialogs
60 {
61 REGISTER_DIALOG(mp_lobby)
62 
63 bool mp_lobby::logout_prompt()
64 {
65  return show_prompt(_("Do you really want to log out?"));
66 }
67 
69  : modal_dialog(window_id())
70  , quit_confirmation(&mp_lobby::logout_prompt)
71  , gamelistbox_(nullptr)
72  , lobby_info_(info)
73  , chatbox_(nullptr)
74  , filter_friends_(register_bool("filter_with_friends",
75  true,
76  []() {return prefs::get().fi_friends_in_game();},
77  [](bool v) {prefs::get().set_fi_friends_in_game(v);},
78  std::bind(&mp_lobby::update_gamelist_filter, this)))
79  , filter_ignored_(register_bool("filter_with_ignored",
80  true,
81  []() {return prefs::get().fi_blocked_in_game();},
82  [](bool v) {prefs::get().set_fi_blocked_in_game(v);},
83  std::bind(&mp_lobby::update_gamelist_filter, this)))
84  , filter_slots_(register_bool("filter_vacant_slots",
85  true,
86  []() {return prefs::get().fi_vacant_slots();},
87  [](bool v) {prefs::get().set_fi_vacant_slots(v);},
88  std::bind(&mp_lobby::update_gamelist_filter, this)))
89  , filter_invert_(register_bool("filter_invert",
90  true,
91  []() {return prefs::get().fi_invert();},
92  [](bool v) {prefs::get().set_fi_invert(v);},
93  std::bind(&mp_lobby::update_gamelist_filter, this)))
94  , filter_auto_hosted_(false)
95  , filter_text_(nullptr)
96  , selected_game_id_()
97  , player_list_(std::bind(&mp_lobby::user_dialog_callback, this, std::placeholders::_1))
98  , player_list_dirty_(true)
99  , gamelist_dirty_(true)
100  , last_lobby_update_()
101  , gamelist_diff_update_(true)
102  , network_connection_(connection)
103  , lobby_update_timer_(0)
104  , gamelist_id_at_row_()
105  , delay_playerlist_update_(false)
106  , delay_gamelist_update_(false)
107  , joined_game_id_(joined_game)
108  , queue_game_scenario_id_()
109  , queue_game_server_preset_()
110 {
111  set_show_even_without_video(true);
112  set_allow_plugin_skip(false);
113  set_always_save_fields(true);
114 
115  /*** Local hotkeys. ***/
117  [](auto&&...) { help::show_help(); return true; });
118 
120  [this](auto&&...) { show_preferences_button_callback(); return true; });
121 }
122 
124 {
126  {
127  l.delay_gamelist_update_ = true;
128  }
130  {
131  l.delay_gamelist_update_ = false;
132  }
134 };
135 
137 {
138  if(lobby_update_timer_) {
140  }
141 }
142 
143 namespace
144 {
145 void modify_grid_with_data(grid* grid, const widget_data& map)
146 {
147  for(const auto& v : map) {
148  const std::string& key = v.first;
149  const widget_item& strmap = v.second;
150 
151  widget* w = grid->find(key, false);
152  if(!w) {
153  continue;
154  }
155 
156  styled_widget* c = dynamic_cast<styled_widget*>(w);
157  if(!c) {
158  continue;
159  }
160 
161  for(const auto & vv : strmap) {
162  if(vv.first == "label") {
163  c->set_label(vv.second);
164  } else if(vv.first == "tooltip") {
165  c->set_tooltip(vv.second);
166  }
167  }
168  }
169 }
170 
171 bool handle_addon_requirements_gui(const std::vector<mp::game_info::required_addon>& reqs, mp::game_info::addon_req addon_outcome)
172 {
173  if(addon_outcome == mp::game_info::addon_req::CANNOT_SATISFY) {
174  std::string e_title = _("Incompatible User-made Content");
175  std::string err_msg = _("This game cannot be joined because the host has out-of-date add-ons that are incompatible with your version. You might wish to suggest that the host’s add-ons be updated.");
176 
177  err_msg +="\n\n";
178  err_msg += _("Details:");
179  err_msg += "\n";
180 
181  for(const mp::game_info::required_addon & a : reqs) {
182  if (a.outcome == mp::game_info::addon_req::CANNOT_SATISFY) {
183  err_msg += font::unicode_bullet + " " + a.message + "\n";
184  }
185  }
186  gui2::show_message(e_title, err_msg, message::auto_close, true);
187 
188  return false;
189  } else if(addon_outcome == mp::game_info::addon_req::NEED_DOWNLOAD) {
190  std::string e_title = _("Missing User-made Content");
191  std::string err_msg = _("This game requires one or more user-made addons to be installed or updated in order to join.\nDo you want to try to install them?");
192 
193  err_msg +="\n\n";
194  err_msg += _("Details:");
195  err_msg += "\n";
196 
197  std::vector<std::string> needs_download;
198  for(const mp::game_info::required_addon & a : reqs) {
199  if(a.outcome == mp::game_info::addon_req::NEED_DOWNLOAD) {
200  err_msg += font::unicode_bullet + " " + a.message + "\n";
201 
202  needs_download.push_back(a.addon_id);
203  }
204  }
205 
206  assert(needs_download.size() > 0);
207 
208  if(gui2::show_message(e_title, err_msg, message::yes_no_buttons, true) == gui2::retval::OK) {
209  // Begin download session
210  try {
211  return ad_hoc_addon_fetch_session(needs_download);
212  } catch (const addons_client::user_exit&) {
213  } catch (const addons_client::user_disconnect&) {
214  }
215  }
216  }
217 
218  return false;
219 }
220 
221 } // end anonymous namespace
222 
224 {
225  if(delay_gamelist_update_) return;
226 
227  SCOPE_LB;
228  gamelistbox_->clear();
229  gamelist_id_at_row_.clear();
230 
231  const auto finish_state_sync = lobby_info_.begin_state_sync();
232 
233  int select_row = -1;
234  for(unsigned i = 0; i < lobby_info_.games().size(); ++i) {
235  const mp::game_info& game = *lobby_info_.games()[i];
236 
237  if(game.id == selected_game_id_) {
238  select_row = i;
239  }
240 
241  gamelist_id_at_row_.push_back(game.id);
242  LOG_LB << "Adding game to listbox (1)" << game.id;
244 
246  }
247 
248  if(select_row >= 0 && select_row != gamelistbox_->get_selected_row()) {
249  gamelistbox_->select_row(select_row);
250  }
251 
253  gamelist_dirty_ = false;
254  last_lobby_update_ = std::chrono::steady_clock::now();
255  finish_state_sync();
257 }
258 
260 {
261  if(delay_gamelist_update_) return;
262 
263  SCOPE_LB;
264  int select_row = -1;
265  unsigned list_i = 0;
266  int list_rows_deleted = 0;
267 
268  const auto finish_state_sync = lobby_info_.begin_state_sync();
269 
270  std::vector<int> next_gamelist_id_at_row;
271  for(unsigned i = 0; i < lobby_info_.games().size(); ++i) {
272  const mp::game_info& game = *lobby_info_.games()[i];
273 
274  if(game.display_status == mp::game_info::disp_status::NEW) {
275  // call void do_notify(notify_mode mode, const std::string& sender, const std::string& message)
276  // sender will be the game_info.scenario (std::string) and message will be game_info.name (std::string)
279  }
280 
281  LOG_LB << "Adding game to listbox " << game.id;
282 
283  if(list_i != gamelistbox_->get_item_count()) {
285  DBG_LB << "Added a game listbox row not at the end" << list_i
286  << " " << gamelistbox_->get_item_count();
287  list_rows_deleted--;
288  } else {
290  }
291 
294 
295  list_i++;
296  next_gamelist_id_at_row.push_back(game.id);
297  } else {
298  if(list_i >= gamelistbox_->get_item_count()) {
299  ERR_LB << "Ran out of listbox items -- triggering a full refresh";
300  refresh_lobby();
301  return;
302  }
303 
304  if(list_i + list_rows_deleted >= gamelist_id_at_row_.size()) {
305  ERR_LB << "gamelist_id_at_row_ overflow! " << list_i << " + "
306  << list_rows_deleted
307  << " >= " << gamelist_id_at_row_.size()
308  << " -- triggering a full refresh";
309  refresh_lobby();
310  return;
311  }
312 
313  int listbox_game_id = gamelist_id_at_row_[list_i + list_rows_deleted];
314  if(game.id != listbox_game_id) {
315  ERR_LB << "Listbox game id does not match expected id "
316  << listbox_game_id << " " << game.id << " (row " << list_i << ")";
317  refresh_lobby();
318  return;
319  }
320 
321  if(game.display_status == mp::game_info::disp_status::UPDATED) {
322  LOG_LB << "Modifying game in listbox " << game.id << " (row " << list_i << ")";
323  grid* grid = gamelistbox_->get_row_grid(list_i);
324  modify_grid_with_data(grid, make_game_row_data(game));
326  ++list_i;
327  next_gamelist_id_at_row.push_back(game.id);
328  } else if(game.display_status == mp::game_info::disp_status::DELETED) {
329  LOG_LB << "Deleting game from listbox " << game.id << " (row "
330  << list_i << ")";
331  gamelistbox_->remove_row(list_i);
332  ++list_rows_deleted;
333  } else {
334  // clean
335  LOG_LB << "Clean game in listbox " << game.id << " (row " << list_i << ")";
336  next_gamelist_id_at_row.push_back(game.id);
337  ++list_i;
338  }
339  }
340  }
341 
342  for(unsigned i = 0; i < next_gamelist_id_at_row.size(); ++i) {
343  if(next_gamelist_id_at_row[i] == selected_game_id_) {
344  select_row = i;
345  }
346  }
347 
348  next_gamelist_id_at_row.swap(gamelist_id_at_row_);
349  if(select_row >= static_cast<int>(gamelistbox_->get_item_count())) {
350  ERR_LB << "Would select a row beyond the listbox" << select_row << " "
352  select_row = gamelistbox_->get_item_count() - 1;
353  }
354 
355  if(select_row >= 0 && select_row != gamelistbox_->get_selected_row()) {
356  gamelistbox_->select_row(select_row);
357  }
358 
360  gamelist_dirty_ = false;
361  last_lobby_update_ = std::chrono::steady_clock::now();
362  finish_state_sync();
364 }
365 
367 {
368  const std::string games_string = VGETTEXT("Games: showing $num_shown out of $num_total", {
369  {"num_shown", std::to_string(lobby_info_.games_visibility().count())},
370  {"num_total", std::to_string(lobby_info_.games().size())}
371  });
372 
373  gamelistbox_->find_widget<label>("map").set_label(games_string);
374 
376 }
377 
379 {
381  widget_item item;
382 
383  item["use_markup"] = "true";
384 
385  color_t color_string;
386  if(game.vacant_slots > 0) {
387  color_string = (game.reloaded || game.started) ? font::YELLOW_COLOR : font::GOOD_COLOR;
388  }
389 
390  const std::string scenario_text = VGETTEXT("$game_name (Era: $era_name)", {
391  {"game_name", game.scenario},
392  {"era_name", game.era}
393  });
394 
395  item["label"] = game.vacant_slots > 0 ? markup::span_color(color_string, game.name) : game.name;
396  data.emplace("name", item);
397 
398  item["label"] = markup::span_color(font::GRAY_COLOR, game.type_marker, markup::italic(scenario_text));
399  data.emplace("scenario", item);
400 
401  item["label"] = markup::span_color(color_string, game.status);
402  data.emplace("status", item);
403 
404  return data;
405 }
406 
408 {
410  grid->find_widget<styled_widget>("status").set_use_markup(true);
411 
412  toggle_panel& row_panel = grid->find_widget<toggle_panel>("panel");
413 
414  //
415  // Game info
416  //
417  std::ostringstream ss;
418 
419  const auto mark_missing = [&ss]() {
420  ss << ' ' << markup::span_color(font::BAD_COLOR, "(", _("era_or_mod^not installed"), ")");
421  };
422 
423  ss << markup::tag("big", markup::span_color(font::TITLE_COLOR, _("Era"))) << "\n" << game.era;
424 
425  if(!game.have_era) {
426  // NOTE: not using colorize() here deliberately to avoid awkward string concatenation.
427  mark_missing();
428  }
429 
430  ss << "\n\n" << markup::tag("big", markup::span_color(font::TITLE_COLOR, _("Modifications"))) << "\n";
431 
432  auto mods = game.mod_info;
433 
434  if(mods.empty()) {
435  ss << _("active_modifications^None") << "\n";
436  } else {
437  for(const auto& mod : mods) {
438  ss << mod.first;
439 
440  if(!mod.second) {
441  mark_missing();
442  }
443 
444  ss << '\n';
445  }
446  }
447 
448  // TODO: move to some general area of the code.
449  const auto yes_or_no = [](bool val) { return val ? _("yes") : _("no"); };
450 
451  ss << "\n" << markup::tag("big", markup::span_color(font::TITLE_COLOR, _("Settings"))) << "\n";
452  ss << _("Experience modifier:") << " " << game.xp << "\n";
453  ss << _("Gold per village:") << " " << game.gold << "\n";
454  ss << _("Map size:") << " " << game.map_size_info << "\n";
455  ss << _("Reloaded:") << " " << yes_or_no(game.reloaded) << "\n";
456  ss << _("Shared vision:") << " " << game.vision << "\n";
457  ss << _("Shuffle sides:") << " " << yes_or_no(game.shuffle_sides) << "\n";
458  ss << _("Time limit:") << " " << game.time_limit << "\n";
459  ss << _("Use map settings:") << " " << yes_or_no(game.use_map_settings);
460 
461  image& info_icon = grid->find_widget<image>("game_info");
462 
463  if(!game.have_era || !game.have_all_mods || !game.required_addons.empty()) {
464  info_icon.set_label("icons/icon-info-error.png");
465 
466  ss << "\n\n" << markup::span_color("#f00", markup::span_size("x-large", "! "));
467  ss << _("One or more add-ons need to be installed\nin order to join this game.");
468  } else {
469  info_icon.set_label("icons/icon-info.png");
470  }
471 
472  info_icon.set_tooltip(ss.str());
473 
474  //
475  // Password icon
476  //
477  image& password_icon = grid->find_widget<image>("needs_password");
478 
479  if(game.password_required) {
481  } else {
483  }
484 
485  //
486  // Observer icon
487  //
488  image& observer_icon = grid->find_widget<image>("observer_icon");
489 
490  if(game.observers) {
491  observer_icon.set_label("misc/eye.png");
492  observer_icon.set_tooltip( _("Observers allowed"));
493  } else {
494  observer_icon.set_label("misc/no_observer.png");
495  observer_icon.set_tooltip( _("Observers not allowed"));
496  }
497 
498  //
499  // Minimap
500  //
501  minimap& map = grid->find_widget<minimap>("minimap");
502 
503  map.set_map_data(game.map_data);
504 
505  if(!add_callbacks) {
506  return;
507  }
508 
510  std::bind(&mp_lobby::enter_game_by_id, this, game.id, DO_EITHER));
511 }
512 
514 {
515  DBG_LB << "mp_lobby::update_gamelist_filter";
517  DBG_LB << "Games in lobby_info: " << lobby_info_.games().size()
518  << ", games in listbox: " << gamelistbox_->get_item_count();
519  assert(lobby_info_.games().size() == gamelistbox_->get_item_count());
520 
522 }
523 
525 {
526  if(delay_playerlist_update_) return;
527 
528  SCOPE_LB;
529  DBG_LB << "Playerlist update: " << lobby_info_.users().size();
530 
532 
533  player_list_dirty_ = false;
534  last_lobby_update_ = std::chrono::steady_clock::now();
535 }
536 
538 {
539  const int idx = gamelistbox_->get_selected_row();
540  bool can_join = false, can_observe = false;
541 
542  if(idx >= 0) {
543  const mp::game_info& game = *lobby_info_.games()[idx];
544  can_observe = game.can_observe();
545  can_join = game.can_join();
546  selected_game_id_ = game.id;
547  } else {
548  selected_game_id_ = 0;
549  }
550 
551  find_widget<button>("observe_global").set_active(can_observe);
552  find_widget<button>("join_global").set_active(can_join);
553 
554  player_list_dirty_ = true;
555 }
556 
558 {
559  SCOPE_LB;
560 
561  gamelistbox_ = find_widget<listbox>("game_list", false, true);
562 
564  std::bind(&mp_lobby::update_selected_game, this));
565 
566  player_list_.init(*this);
567 
568  set_enter_disabled(true);
569 
570  // Exit hook to add a confirmation when quitting the Lobby.
571  set_exit_hook(window::exit_hook::always, [this] { return get_retval() == retval::CANCEL ? quit() : true; });
572 
573  chatbox_ = find_widget<chatbox>("chat", false, true);
574 
576 
579 
580  find_widget<button>("create").set_retval(CREATE);
581 
583  find_widget<button>("show_preferences"),
585 
587  find_widget<button>("join_global"),
588  std::bind(&mp_lobby::enter_selected_game, this, DO_JOIN));
589 
590  find_widget<button>("join_global").set_active(false);
591 
593  find_widget<button>("observe_global"),
594  std::bind(&mp_lobby::enter_selected_game, this, DO_OBSERVE));
595 
597  find_widget<button>("server_info"),
598  std::bind(&mp_lobby::show_server_info, this));
599 
600  find_widget<button>("observe_global").set_active(false);
601 
602  menu_button& replay_options = find_widget<menu_button>("replay_options");
603 
604  if(prefs::get().skip_mp_replay()) {
605  replay_options.set_selected(1);
606  }
607 
608  if(prefs::get().blindfold_replay()) {
609  replay_options.set_selected(2);
610  }
611 
612  connect_signal_notify_modified(replay_options,
613  std::bind(&mp_lobby::skip_replay_changed_callback, this));
614 
615  filter_text_ = find_widget<text_box>("filter_text", false, true);
616 
618  *filter_text_,
619  std::bind(&mp_lobby::game_filter_keypress_callback, this, std::placeholders::_5));
620 
621  chatbox_->room_window_open(N_("lobby"), true, false);
623 
625 
626  // Force first update to be directly.
627  update_gamelist();
629 
630  // TODO: currently getting a crash in the chatbox if we use
631  // -- vultraz, 2017-11-10
632  //mp_lobby::network_handler();
633 
636 
637  //
638  // Profile box
639  //
640  if(panel* profile_panel = find_widget<panel>("profile", false, false)) {
641  auto your_info = std::find_if(lobby_info_.users().begin(), lobby_info_.users().end(),
642  [](const auto& u) { return u.get_relation() == mp::user_info::user_relation::ME; });
643 
644  if(your_info != lobby_info_.users().end()) {
645  profile_panel->find_widget<label>("username").set_label(your_info->name);
646 
647  auto& profile_button = profile_panel->find_widget<button>("view_profile");
648  connect_signal_mouse_left_click(profile_button, std::bind(&mp_lobby::open_profile_url, this));
649 
650  auto& history_button = profile_panel->find_widget<button>("view_match_history");
651  connect_signal_mouse_left_click(history_button, std::bind(&mp_lobby::open_match_history, this));
652  }
653  }
654 
655  listbox& tab_bar = find_widget<listbox>("games_list_tab_bar");
657 
658  // server-side queue join button
659  button* queue_join_button = find_widget<button>("join_queue", false, true);
660  connect_signal_mouse_left_click(*queue_join_button, std::bind(&mp_lobby::join_queue, this));
661 
662  // server-side queue leave button
663  button* queue_leave_button = find_widget<button>("leave_queue", false, true);
664  connect_signal_mouse_left_click(*queue_leave_button, std::bind(&mp_lobby::leave_queue, this));
665 
666  // server-side queues list
668 
669  // Set up Lua plugin context
670  plugins_context_.reset(new plugins_context("Multiplayer Lobby"));
671 
672  plugins_context_->set_callback("join", [&, this](const config&) {
674  }, true);
675 
676  plugins_context_->set_callback("observe", [&, this](const config&) {
678  }, true);
679 
680  plugins_context_->set_callback("create", [this](const config&) { set_retval(CREATE); }, true);
681  plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
682 
683  plugins_context_->set_callback("chat", [this](const config& cfg) { chatbox_->send_chat_message(cfg["message"], false); }, true);
684  plugins_context_->set_callback("select_game", [this](const config& cfg) {
685  selected_game_id_ = cfg.has_attribute("id") ? cfg["id"].to_int() : lobby_info_.games()[cfg["index"].to_int()]->id;
686  }, true);
687 
688  plugins_context_->set_accessor("game_list", [this](const config&) { return lobby_info_.gamelist(); });
689 }
690 
692 {
693  filter_auto_hosted_ = find_widget<listbox>("games_list_tab_bar").get_selected_row() == 1;
695 }
696 
698 {
700  if(info && info->forum_id != 0) {
702  }
703 }
704 
706 {
707  listbox* queues_listbox = find_widget<listbox>("queue_list", false, true);
708  const std::vector<mp::queue_info>& queues = mp::get_server_queues();
709  if(queues.size() > static_cast<std::size_t>(queues_listbox->get_selected_row())) {
710  const mp::queue_info& queue = queues[queues_listbox->get_selected_row()];
711  config join_server_queue;
712 
713  config& queue_req = join_server_queue.add_child("join_server_queue");
714  queue_req["queue_id"] = queue.id;
715  mp::send_to_server(join_server_queue);
716  } else {
717  ERR_LB << "Attempted to join queue but couldn't find queue info";
718  }
719 }
720 
722 {
723  listbox* queues_listbox = find_widget<listbox>("queue_list", false, true);
724  const std::vector<mp::queue_info>& queues = mp::get_server_queues();
725  if(queues.size() > static_cast<std::size_t>(queues_listbox->get_selected_row())) {
726  const mp::queue_info& queue = queues[queues_listbox->get_selected_row()];
727  config leave_server_queue;
728 
729  config& queue_req = leave_server_queue.add_child("leave_server_queue");
730  queue_req["queue_id"] = queue.id;
731  mp::send_to_server(leave_server_queue);
732  } else {
733  ERR_LB << "Attempted to join queue but couldn't find queue info";
734  }
735 }
736 
738 {
741  plugins_context_.reset();
742 }
743 
745 {
747  if(info) {
749  }
750 }
751 
753 {
754  try {
755  config data;
758  }
759  } catch (const wesnothd_error& e) {
760  LOG_LB << "caught wesnothd_error in network_handler: " << e.message;
761  throw;
762  }
763 
764  if(std::chrono::steady_clock::now() - last_lobby_update_ < game_config::lobby_refresh) {
765  return;
766  }
767 
769  //don't process a corrupted gamelist further to prevent crashes later.
770  return;
771  }
772 
776  } else {
777  update_gamelist();
778  gamelist_diff_update_ = true;
779  }
780  }
781 
785  }
786 }
787 
789 {
790  listbox* queues_listbox = find_widget<listbox>("queue_list", false, true);
791  queues_listbox->clear();
792 
793  std::vector<mp::queue_info>& queues = mp::get_server_queues();
794  std::sort(queues.begin(), queues.end(), [](const mp::queue_info& q1, const mp::queue_info& q2) { return q1.display_name < q2.display_name; });
795 
796  for(const mp::queue_info& info : queues) {
798  widget_item item;
799 
800  item["label"] = info.current_players.count(prefs::get().login()) > 0 ? "x" : "o";
801  data.emplace("is_in_queue", item);
802 
803  item["label"] = info.display_name;
804  data.emplace("queue_name", item);
805 
806  item["label"] = std::to_string(info.current_players.size())+"/"+std::to_string(info.players_required);
807  data.emplace("queue_player_count", item);
808 
809  queues_listbox->add_row(data);
810  }
811 }
812 
814 {
815  if(auto error = data.optional_child("error")) {
816  throw wesnothd_error(error["message"]);
817  } else if(data.has_child("gamelist")) {
819  } else if(auto gamelist_diff = data.optional_child("gamelist_diff")) {
820  process_gamelist_diff(*gamelist_diff);
821  } else if(auto info = data.optional_child("message")) {
822  if(info["type"] == "server_info") {
823  server_information_ = info["message"].str();
824  return;
825  } else if(info["type"] == "announcements") {
826  announcements_ = info["message"].str();
827  return;
828  }
829  } else if(auto create = data.optional_child("create_game")) {
830  queue_game_scenario_id_ = create["mp_scenario"].str();
831  queue_game_server_preset_ = create.value().mandatory_child("game");
832  queue_id_ = create["queue_id"].to_int();
834  return;
835  } else if(auto join_game = data.optional_child("join_game")) {
836  enter_game_by_id(join_game["id"].to_int(), JOIN_MODE::DO_JOIN);
837  return;
838  } else if(auto queue_update = data.optional_child("queue_update")) {
839  std::vector<mp::queue_info>& queues = mp::get_server_queues();
840  if(queue_update["action"].str() == "add") {
841  mp::queue_info& new_info = queues.emplace_back();
842  new_info.id = queue_update["queue_id"].to_int();
843  new_info.players_required = queue_update["players_required"].to_int();
844  new_info.display_name = queue_update["display_name"].str();
845  new_info.scenario_id = queue_update["scenario_id"].str();
846  } else {
847  for(mp::queue_info& info : queues) {
848  if(info.id == queue_update["queue_id"].to_int()) {
849  if(queue_update["action"].str() == "remove") {
850  utils::erase_if(queues, [&](const mp::queue_info& i) { return i.id == queue_update["queue_id"].to_int(); });
851  } else if(queue_update["action"].str() == "update") {
852  if(queue_update->has_attribute("scenario_id")) {
853  info.scenario_id = queue_update["scenario_id"].str();
854  }
855  if(queue_update->has_attribute("display_name")) {
856  info.display_name = queue_update["display_name"].str();
857  }
858  if(queue_update->has_attribute("players_required")) {
859  info.players_required = queue_update["players_required"].to_int();
860  }
861  if(queue_update->has_attribute("current_players")){
862  info.current_players = utils::split_set(queue_update["current_players"].str());
863  }
864  } else {
865  continue;
866  }
867  }
868  }
869  }
870 
872 
873  return;
874  }
875 
877 }
878 
880 {
882 
884  DBG_LB << "Received gamelist";
885  gamelist_dirty_ = true;
886  gamelist_diff_update_ = false;
887 }
888 
890 {
892 
894  DBG_LB << "Received gamelist diff";
895  gamelist_dirty_ = true;
896  } else {
897  ERR_LB << "process_gamelist_diff failed!";
898  refresh_lobby();
899  }
900  const int joined = data.child_count("insert_child");
901  const int left = data.child_count("remove_child");
902  if(joined > 0 || left > 0) {
903  if(left > joined) {
905  } else {
907  }
908  }
909 }
910 
912 {
913  switch(mode) {
914  case DO_JOIN:
915  if(!game.can_join()) {
916  ERR_LB << "Attempted to join a game with no vacant slots";
917  return;
918  }
919 
920  break;
921  case DO_OBSERVE:
922  if(!game.can_observe()) {
923  ERR_LB << "Attempted to observe a game with observers disabled";
924  return;
925  }
926 
927  break;
928  case DO_EITHER:
929  if(game.can_join()) {
930  mode = DO_JOIN;
931  } else if(game.can_observe()) {
932  mode = DO_OBSERVE;
933  } else {
934  DBG_LB << "Cannot join or observe a game.";
935  return;
936  }
937 
938  break;
939  }
940 
941  // prompt moderators for whether they want to join a game with observers disabled
942  if(!game.observers && mp::logged_in_as_moderator()) {
943  if(gui2::show_message(_("Observe"), _("This game doesn’t allow observers. Observe using moderator rights anyway?"), gui2::dialogs::message::yes_no_buttons) != gui2::retval::OK) {
944  return;
945  }
946  }
947 
948  const bool try_join = mode == DO_JOIN;
949  const bool try_obsv = mode == DO_OBSERVE;
950 
951  // Prompt user to download this game's required addons if its requirements have not been met
952  if(game.addons_outcome != mp::game_info::addon_req::SATISFIED) {
953  if(game.required_addons.empty()) {
954  gui2::show_error_message(_("Something is wrong with the addon version check database supporting the multiplayer lobby. Please report this at https://bugs.wesnoth.org."));
955  return;
956  }
957 
958  if(!handle_addon_requirements_gui(game.required_addons, game.addons_outcome)) {
959  return;
960  }
961 
962  // Addons have been downloaded, so the game_config and installed addons list need to be reloaded.
963  // The lobby is closed and reopened.
965  return;
966  }
967 
968  config response;
969 
970  config& join_data = response.add_child("join");
971  join_data["id"] = std::to_string(game.id);
972  join_data["observe"] = try_obsv;
973 
974  if(mp::logged_in_as_moderator() && game.password_required) {
975  if(gui2::show_message(_("Join"), _("This game is password protected. Join using moderator rights anyway?"), gui2::dialogs::message::yes_no_buttons) != gui2::retval::OK) {
976  return;
977  }
978  } else if(!join_data.empty() && game.password_required) {
979  std::string password;
980 
981  if(!gui2::dialogs::mp_join_game_password_prompt::execute(password)) {
982  return;
983  }
984 
985  join_data["password"] = password;
986  }
987 
988  join_data["mp_scenario"] = game.scenario_id;
989  mp::send_to_server(response);
990 
991  if(game.id >= 0) {
992  joined_game_id_ = game.id;
993 
994  // We're all good. Close lobby and proceed to game!
995  set_retval(try_join ? JOIN : OBSERVE);
996  }
997 }
998 
1000 {
1001  try {
1003  } catch(const std::out_of_range&) {
1004  // Game index was invalid!
1005  ERR_LB << "Attempted to join/observe a game with index out of range: " << index << ". "
1006  << "Games vector size is " << lobby_info_.games().size();
1007  }
1008 }
1009 
1010 void mp_lobby::enter_game_by_id(const int game_id, JOIN_MODE mode)
1011 {
1012  mp::game_info* game_ptr = lobby_info_.get_game_by_id(game_id);
1013 
1014  if(!game_ptr) {
1015  ERR_LB << "Attempted to join/observe a game with an invalid id: " << game_id;
1016  return;
1017  }
1018 
1019  enter_game(*game_ptr, mode);
1020 }
1021 
1023 {
1025 }
1026 
1028 {
1029  mp::send_to_server(config("refresh_lobby"));
1030 }
1031 
1033 {
1034  gui2::dialogs::preferences_dialog::display();
1035 
1036  refresh_lobby();
1037 }
1038 
1040 {
1042 }
1043 
1045 {
1047 
1049  for(const auto& s : utils::split(filter_text_->get_value(), ' ')) {
1050  if(!info.match_string_filter(s)) {
1051  return false;
1052  }
1053  }
1054 
1055  return true;
1056  });
1057 
1058  lobby_info_.add_game_filter([this](const mp::game_info& info) {
1059  return filter_friends_->get_widget_value() ? info.has_friends == true : true;
1060  });
1061 
1062  // Unlike the friends filter, this is an inclusion filter (do we want to also show
1063  // games with blocked players) rather than an exclusion filter (do we want to show
1064  // only games with friends).
1065  lobby_info_.add_game_filter([this](const mp::game_info& info) {
1066  return filter_ignored_->get_widget_value() == false ? info.has_ignored == false : true;
1067  });
1068 
1069  lobby_info_.add_game_filter([this](const mp::game_info& info) {
1070  return filter_slots_->get_widget_value() ? info.vacant_slots > 0 : true;
1071  });
1072 
1073  lobby_info_.add_game_filter([this](const mp::game_info& info) {
1074  return info.auto_hosted == filter_auto_hosted_;
1075  });
1076 
1077  lobby_info_.set_game_filter_invert(
1078  [this](bool val) { return filter_invert_->get_widget_value() ? !val : val; });
1079 }
1080 
1081 void mp_lobby::game_filter_keypress_callback(const SDL_Keycode key)
1082 {
1083  if(key == SDLK_RETURN || key == SDLK_KP_ENTER) {
1085  }
1086 }
1087 
1089 {
1090  delay_playerlist_update_ = true;
1092 
1094  dlg.show();
1095 
1096  if(dlg.result_open_whisper()) {
1099  }
1100 
1101  selected_game_id_ = info->game_id;
1102 
1103  // do not update here as it can cause issues with removing the widget
1104  // from within it's event handler. Should get updated as soon as possible
1105  // update_gamelist();
1106  delay_playerlist_update_ = false;
1107  player_list_dirty_ = true;
1108  refresh_lobby();
1109 }
1110 
1112 {
1113  // TODO: this prefence should probably be controlled with an enum
1114  const int value = find_widget<menu_button>("replay_options").get_value();
1115  prefs::get().set_skip_mp_replay(value == 1);
1116  prefs::get().set_blindfold_replay(value == 2);
1117 }
1118 
1119 } // namespace dialogs
double t
Definition: astarsearch.cpp:63
double g
Definition: astarsearch.cpp:63
std::map< std::string, chatroom_log > default_chat_log
Definition: chat_log.cpp:18
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:362
bool has_attribute(config_key_type key) const
Definition: config.cpp:157
bool empty() const
Definition: config.cpp:845
config & add_child(config_key_type key)
Definition: config.cpp:436
Simple push button.
Definition: button.hpp:36
lobby_chat_window * room_window_open(const std::string &room, const bool open_new, const bool allow_close=true)
Check if a room window for "room" is open, if open_new is true then it will be created if not found.
Definition: chatbox.cpp:381
virtual void send_chat_message(const std::string &message, bool allies_only) override
Inherited form chat_handler.
Definition: chatbox.cpp:253
lobby_chat_window * whisper_window_open(const std::string &name, bool open_new)
Check if a whisper window for user "name" is open, if open_new is true then it will be created if not...
Definition: chatbox.cpp:387
void switch_to_window(lobby_chat_window *t)
Switch to the window given by a valid pointer (e.g.
Definition: chatbox.cpp:130
void process_network_data(const ::config &data)
Definition: chatbox.cpp:630
void set_active_window_changed_callback(const std::function< void(void)> &f)
Definition: chatbox.hpp:81
void load_log(std::map< std::string, chatroom_log > &log, bool show_lobby)
Definition: chatbox.cpp:90
void active_window_changed()
Definition: chatbox.cpp:115
@ yes_no_buttons
Shows a yes and no button.
Definition: message.hpp:81
@ auto_close
Enables auto close.
Definition: message.hpp:71
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
void enter_selected_game(JOIN_MODE mode)
Enter game by index, where index is the selected game listbox row.
Definition: lobby.cpp:1022
void process_gamelist(const config &data)
Definition: lobby.cpp:879
void update_visible_games()
Definition: lobby.cpp:366
void update_selected_game()
Definition: lobby.cpp:537
virtual void post_show() override
Actions to be taken after the window has been shown.
Definition: lobby.cpp:737
mp::lobby_info & lobby_info_
Definition: lobby.hpp:149
text_box * filter_text_
Definition: lobby.hpp:159
lobby_player_list_helper player_list_
Definition: lobby.hpp:163
void skip_replay_changed_callback()
Definition: lobby.cpp:1111
static std::string announcements_
Definition: lobby.hpp:193
wesnothd_connection & network_connection_
Definition: lobby.hpp:173
void update_gamelist_filter()
Definition: lobby.cpp:513
listbox * gamelistbox_
Definition: lobby.hpp:147
void process_network_data(const config &data)
Definition: lobby.cpp:813
widget_data make_game_row_data(const mp::game_info &game)
Definition: lobby.cpp:378
void game_filter_keypress_callback(const SDL_Keycode key)
Definition: lobby.cpp:1081
void enter_game_by_id(const int game_id, JOIN_MODE mode)
Entry wrapper for enter_game, where game is located by game id.
Definition: lobby.cpp:1010
void update_gamelist_diff()
Definition: lobby.cpp:259
void show_preferences_button_callback()
Definition: lobby.cpp:1032
static std::string server_information_
Definition: lobby.hpp:192
void enter_game(const mp::game_info &game, JOIN_MODE mode)
Exits the lobby and enters the given game.
Definition: lobby.cpp:911
std::string queue_game_scenario_id_
Definition: lobby.hpp:186
void user_dialog_callback(const mp::user_info *info)
Definition: lobby.cpp:1088
void tab_switch_callback()
Definition: lobby.cpp:691
mp_lobby(mp::lobby_info &info, wesnothd_connection &connection, int &joined_game)
Definition: lobby.cpp:68
void process_gamelist_diff(const config &data)
Definition: lobby.cpp:889
std::size_t lobby_update_timer_
Timer for updating the lobby.
Definition: lobby.hpp:176
config queue_game_server_preset_
Definition: lobby.hpp:187
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: lobby.cpp:557
void adjust_game_row_contents(const mp::game_info &game, grid *grid, bool add_callbacks=true)
Definition: lobby.cpp:407
std::vector< int > gamelist_id_at_row_
Definition: lobby.hpp:178
std::chrono::steady_clock::time_point last_lobby_update_
Definition: lobby.hpp:169
void network_handler()
Network polling callback.
Definition: lobby.cpp:752
void enter_game_by_index(const int index, JOIN_MODE mode)
Entry wrapper for enter_game, where game is located by index.
Definition: lobby.cpp:999
@ RELOAD_CONFIG
player clicked the Create button
Definition: lobby.hpp:65
@ CREATE_PRESET
player clicked Join button on an [mp_queue] game, but there was no existing game to join
Definition: lobby.hpp:66
static void display(const std::string &player_name, wesnothd_connection &connection, bool wait_for_response=true)
The display function.
std::unique_ptr< plugins_context > plugins_context_
static void display(const std::string &info, const std::string &announcements)
The display function.
void register_hotkey(const hotkey::HOTKEY_COMMAND id, Func &&function)
Registers a hotkey.
Definition: dispatcher.hpp:444
Base container class.
Definition: grid.hpp:32
widget * find(const std::string_view id, const bool must_be_active) override
See widget::find.
Definition: grid.cpp:645
The listbox class.
Definition: listbox.hpp:41
void set_row_shown(const unsigned row, const bool shown)
Makes a row visible or invisible.
Definition: listbox.cpp:171
grid & add_row(const widget_item &item, const int index=-1)
When an item in the list is selected by the user we need to update the state.
Definition: listbox.cpp:92
const grid * get_row_grid(const unsigned row) const
Returns the grid of the wanted row.
Definition: listbox.cpp:267
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition: listbox.cpp:280
void remove_row(const unsigned row, unsigned count=1)
Removes a row in the listbox.
Definition: listbox.cpp:112
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:153
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:305
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition: listbox.cpp:159
const mp::user_info * get_selected_info() const
void update(const std::vector< mp::user_info > &user_info, int focused_game)
Updates the tree contents based on the given user data.
void set_selected(unsigned selected, bool fire_event=true)
void set_map_data(const std::string &map_data)
Definition: minimap.cpp:74
void set_tooltip(const t_string &tooltip)
virtual void set_label(const t_string &text)
virtual void set_use_markup(bool use_markup)
std::string get_value() const
Base class for all widgets.
Definition: widget.hpp:55
void set_visible(const visibility visible)
Definition: widget.cpp:479
@ visible
The user sets the widget visible, that means:
@ hidden
The user sets the widget hidden, that means:
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:747
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:321
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:395
show_mode mode() const
Returns the dialog mode for this window.
Definition: window.hpp:695
void keyboard_capture(widget *widget)
Definition: window.cpp:1201
void set_exit_hook(exit_hook mode, const Func &hook)
Sets the window's exit hook.
Definition: window.hpp:448
int get_retval()
Definition: window.hpp:402
This class represents the collective information the client has about the players and games on the se...
Definition: lobby_info.hpp:31
void add_game_filter(game_filter_func func)
Adds a new filter function to be considered when apply_game_filter is called.
Definition: lobby_info.hpp:65
bool is_game_visible(const game_info &)
Returns whether the game would be visible after the game filters are applied.
Definition: lobby_info.cpp:381
void clear_game_filters()
Clears all game filter functions.
Definition: lobby_info.hpp:71
const config & gamelist() const
Returns the raw game list config data.
Definition: lobby_info.hpp:57
void process_gamelist(const config &data)
Process a full game list.
Definition: lobby_info.cpp:115
bool process_gamelist_diff(const config &data)
Process a gamelist diff.
Definition: lobby_info.cpp:190
std::function< void()> begin_state_sync()
Updates the game pointer list and returns a second stage cleanup function to be called after any acti...
Definition: lobby_info.cpp:311
game_info * get_game_by_id(int id)
Returns info on a game with the given game ID.
Definition: lobby_info.cpp:343
bool gamelist_initialized() const
Definition: lobby_info.hpp:117
const std::vector< game_info * > & games() const
Definition: lobby_info.hpp:97
void apply_game_filter()
Generates a new list of games that match the current filter functions and inversion setting.
Definition: lobby_info.cpp:392
const boost::dynamic_bitset & games_visibility() const
Definition: lobby_info.hpp:102
const std::vector< user_info > & users() const
Definition: lobby_info.hpp:107
static prefs & get()
Implements a quit confirmation dialog.
static bool quit()
Shows the quit confirmation if needed.
A class that represents a TCP/IP connection to the wesnothd server.
bool receive_data(config &result)
Receives the next pending data pack from the server, if available.
Networked add-ons (campaignd) client interface.
Implements some helper classes to ease adding fields to a dialog and hide the synchronization needed.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1030
int w
#define N_(String)
Definition: gettext.hpp:105
static std::string _(const char *str)
Definition: gettext.hpp:97
#define LOG_LB
Definition: lobby.cpp:55
#define ERR_LB
Definition: lobby.cpp:56
static lg::log_domain log_lobby("lobby")
#define DBG_LB
Definition: lobby.cpp:54
#define SCOPE_LB
Definition: lobby.cpp:57
bool ad_hoc_addon_fetch_session(const std::vector< std::string > &addon_ids)
Conducts an ad-hoc add-ons server connection to download an add-on with a particular id and all it's ...
Definition: manager_ui.cpp:255
bool open_object([[maybe_unused]] const std::string &path_or_url)
Definition: open.cpp:46
const color_t YELLOW_COLOR
const color_t GOOD_COLOR
const color_t BAD_COLOR
const color_t TITLE_COLOR
const color_t GRAY_COLOR
const std::string unicode_bullet
Definition: constants.cpp:47
std::chrono::milliseconds lobby_refresh
Definition: game_config.cpp:72
std::chrono::milliseconds lobby_network_timer
Definition: game_config.cpp:71
REGISTER_DIALOG(editor_edit_unit)
void connect_signal_pre_key_press(dispatcher &dispatcher, const signal_keyboard &signal)
Connects the signal for 'snooping' on the keypress.
Definition: dispatcher.cpp:158
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:189
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:163
void connect_signal_mouse_left_double_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button double click.
Definition: dispatcher.cpp:184
std::size_t add_timer(const std::chrono::milliseconds &interval, const std::function< void(std::size_t id)> &callback, const bool repeat)
Adds a new timer.
Definition: timer.cpp:123
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:36
std::map< std::string, t_string > widget_item
Definition: widget.hpp:33
void show_error_message(const std::string &msg, bool message_use_markup)
Shows an error message to the user.
Definition: message.cpp:201
bool remove_timer(const std::size_t id)
Removes a timer.
Definition: timer.cpp:164
void show_message(const std::string &title, const std::string &msg, const std::string &button_caption, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Shows a message to the user.
Definition: message.cpp:148
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition: retval.hpp:38
void show_help(const std::string &show_topic)
Open the help browser, show topic with id show_topic.
Definition: help.cpp:139
@ HOTKEY_PREFERENCES
Functions to load and save images from/to disk.
logger & info()
Definition: log.cpp:318
std::string italic(Args &&... data)
Applies italic Pango markup to the input.
Definition: markup.hpp:176
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
std::string tag(std::string_view tag, Args &&... data)
Wraps the given data in the specified tag.
Definition: markup.hpp:45
std::string span_size(std::string_view size, Args &&... data)
Applies Pango markup to the input specifying its display size.
Definition: markup.hpp:146
void do_notify(notify_mode mode, const std::string &sender, const std::string &message)
Definition: lobby_info.cpp:57
void send_to_server(const config &data)
Attempts to send given data to server if a connection is open.
std::vector< queue_info > & get_server_queues()
Gets the list of server-side queues received on login.
std::string get_profile_link(int user_id)
Gets the forum profile link for the given user.
bool logged_in_as_moderator()
Gets whether the currently logged-in user is a moderator.
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
std::set< std::string > split_set(std::string_view s, char sep, const int flags)
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition: general.hpp:106
std::vector< std::string > split(const config_attribute_value &val)
Desktop environment interaction functions.
std::string_view data
Definition: picture.cpp:188
The basic class for representing 8-bit RGB or RGBA colour values.
Definition: color.hpp:61
This class represents the info a client has about a game on the server.
Definition: lobby_data.hpp:62
std::string display_name
Definition: multiplayer.hpp:34
std::string scenario_id
Definition: multiplayer.hpp:33
This class represents the information a client has about another player.
Definition: lobby_data.hpp:29
An error occurred during when trying to communicate with the wesnothd server.
mock_char c
static map_location::direction s
Contains the gui2 timer routines.
#define e