The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
25
26#include "gui/core/timer.hpp"
28#include "gui/widgets/image.hpp"
29#include "gui/widgets/label.hpp"
38
39#include "addon/client.hpp"
40#include "addon/manager.hpp"
41#include "addon/manager_ui.hpp"
42#include "chat_log.hpp"
43#include "desktop/open.hpp"
45#include "formatter.hpp"
48#include "gettext.hpp"
49#include "help/help.hpp"
51
52#include <functional>
53
54static lg::log_domain log_lobby("lobby");
55#define DBG_LB LOG_STREAM(debug, log_lobby)
56#define LOG_LB LOG_STREAM(info, log_lobby)
57#define ERR_LB LOG_STREAM(err, log_lobby)
58#define SCOPE_LB log_scope2(log_lobby, __func__)
59
60namespace gui2::dialogs
61{
62REGISTER_DIALOG(mp_lobby)
63
64bool mp_lobby::logout_prompt()
65{
66 return show_prompt(_("Do you really want to log out?"));
67}
68
69mp_lobby::mp_lobby(mp::lobby_info& info, wesnothd_connection& connection, int& joined_game)
70 : modal_dialog(window_id())
71 , quit_confirmation(&mp_lobby::logout_prompt)
72 , gamelistbox_(nullptr)
73 , lobby_info_(info)
74 , chatbox_(nullptr)
75 , filter_friends_(register_bool("filter_with_friends",
76 true,
77 []() {return prefs::get().fi_friends_in_game();},
78 [](bool v) {prefs::get().set_fi_friends_in_game(v);},
79 std::bind(&mp_lobby::update_gamelist_filter, this)))
80 , filter_ignored_(register_bool("filter_with_ignored",
81 true,
82 []() {return prefs::get().fi_blocked_in_game();},
83 [](bool v) {prefs::get().set_fi_blocked_in_game(v);},
84 std::bind(&mp_lobby::update_gamelist_filter, this)))
85 , filter_slots_(register_bool("filter_vacant_slots",
86 true,
87 []() {return prefs::get().fi_vacant_slots();},
88 [](bool v) {prefs::get().set_fi_vacant_slots(v);},
89 std::bind(&mp_lobby::update_gamelist_filter, this)))
90 , filter_invert_(register_bool("filter_invert",
91 true,
92 []() {return prefs::get().fi_invert();},
93 [](bool v) {prefs::get().set_fi_invert(v);},
94 std::bind(&mp_lobby::update_gamelist_filter, this)))
95 , filter_auto_hosted_(false)
96 , filter_game_presets_(false)
97 , filter_text_(nullptr)
98 , selected_game_id_()
99 , player_list_(std::bind(&mp_lobby::user_dialog_callback, this, std::placeholders::_1),
100 std::bind(&mp_lobby::select_game_by_player, this, std::placeholders::_1))
101 , player_list_dirty_(true)
102 , gamelist_dirty_(true)
103 , last_lobby_update_()
104 , gamelist_diff_update_(true)
105 , network_connection_(connection)
106 , lobby_update_timer_(0)
107 , gamelist_id_at_row_()
108 , delay_playerlist_update_(false)
109 , delay_gamelist_update_(false)
110 , joined_game_id_(joined_game)
111 , queue_game_server_preset_()
112{
113 set_show_even_without_video(true);
114 set_allow_plugin_skip(false);
115 set_always_save_fields(true);
116
117 /*** Local hotkeys. ***/
119 [](auto&&...) { help::show_help(); return true; });
120
122 [this](auto&&...) { show_preferences_button_callback(); return true; });
123}
124
137
144
145namespace
146{
147void modify_grid_with_data(grid* grid, const widget_data& map)
148{
149 for(const auto& v : map) {
150 const std::string& key = v.first;
151 const widget_item& strmap = v.second;
152
153 widget* w = grid->find(key, false);
154 if(!w) {
155 continue;
156 }
157
158 styled_widget* c = dynamic_cast<styled_widget*>(w);
159 if(!c) {
160 continue;
161 }
162
163 for(const auto & vv : strmap) {
164 if(vv.first == "label") {
165 c->set_label(vv.second);
166 } else if(vv.first == "tooltip") {
167 c->set_tooltip(vv.second);
168 }
169 }
170 }
171}
172
173bool handle_addon_requirements_gui(const std::vector<mp::game_info::required_addon>& reqs, mp::game_info::addon_req addon_outcome)
174{
175 if(addon_outcome == mp::game_info::addon_req::CANNOT_SATISFY) {
176 std::string e_title = _("Incompatible User-made Content");
177 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.");
178
179 err_msg +="\n\n";
180 err_msg += _("Details:");
181 err_msg += "\n";
182
183 for(const mp::game_info::required_addon & a : reqs) {
185 err_msg += font::unicode_bullet + " " + a.message + "\n";
186 }
187 }
188 gui2::show_message(e_title, err_msg, message::auto_close, true);
189
190 return false;
191 } else if(addon_outcome == mp::game_info::addon_req::NEED_DOWNLOAD) {
192 std::string e_title = _("Missing User-made Content");
193 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?");
194
195 err_msg +="\n\n";
196 err_msg += _("Details:");
197 err_msg += "\n";
198
199 std::vector<std::string> needs_download;
200 for(const mp::game_info::required_addon & a : reqs) {
202 err_msg += font::unicode_bullet + " " + a.message + "\n";
203
204 needs_download.push_back(a.addon_id);
205 }
206 }
207
208 assert(needs_download.size() > 0);
209
210 if(gui2::show_message(e_title, err_msg, message::yes_no_buttons, true) == gui2::retval::OK) {
211 // Begin download session
212 try {
213 return ad_hoc_addon_fetch_session(needs_download);
214 } catch (const addons_client::user_exit&) {
215 } catch (const addons_client::user_disconnect&) {
216 }
217 }
218 }
219
220 return false;
221}
222
223} // end anonymous namespace
224
226{
227 if(delay_gamelist_update_) return;
228
229 SCOPE_LB;
231 gamelist_id_at_row_.clear();
232
233 const auto finish_state_sync = lobby_info_.begin_state_sync();
234
235 int select_row = -1;
236 for(unsigned i = 0; i < lobby_info_.games().size(); ++i) {
237 const mp::game_info& game = *lobby_info_.games()[i];
238
239 if(game.id == selected_game_id_) {
240 select_row = i;
241 }
242
243 gamelist_id_at_row_.push_back(game.id);
244 LOG_LB << "Adding game to listbox (1)" << game.id;
246
248 }
249
250 if(select_row >= 0 && select_row != gamelistbox_->get_selected_row()) {
251 gamelistbox_->select_row(select_row);
252 }
253
255 gamelist_dirty_ = false;
256 last_lobby_update_ = std::chrono::steady_clock::now();
257 finish_state_sync();
259}
260
262{
263 if(delay_gamelist_update_) return;
264
265 SCOPE_LB;
266 int select_row = -1;
267 unsigned list_i = 0;
268 int list_rows_deleted = 0;
269
270 const auto finish_state_sync = lobby_info_.begin_state_sync();
271
272 std::vector<int> next_gamelist_id_at_row;
273 for(unsigned i = 0; i < lobby_info_.games().size(); ++i) {
274 const mp::game_info& game = *lobby_info_.games()[i];
275
276 if(game.display_status == mp::game_info::disp_status::NEW) {
277 // call void do_notify(notify_mode mode, const std::string& sender, const std::string& message)
278 // sender will be the game_info.scenario (std::string) and message will be game_info.name (std::string)
280 do_notify(mp::notify_mode::game_created, game.scenario, game.name);
281 }
282
283 LOG_LB << "Adding game to listbox " << game.id;
284
285 if(list_i != gamelistbox_->get_item_count()) {
287 DBG_LB << "Added a game listbox row not at the end" << list_i
288 << " " << gamelistbox_->get_item_count();
289 list_rows_deleted--;
290 } else {
292 }
293
296
297 list_i++;
298 next_gamelist_id_at_row.push_back(game.id);
299 } else {
300 if(list_i >= gamelistbox_->get_item_count()) {
301 ERR_LB << "Ran out of listbox items -- triggering a full refresh";
303 return;
304 }
305
306 if(list_i + list_rows_deleted >= gamelist_id_at_row_.size()) {
307 ERR_LB << "gamelist_id_at_row_ overflow! " << list_i << " + "
308 << list_rows_deleted
309 << " >= " << gamelist_id_at_row_.size()
310 << " -- triggering a full refresh";
312 return;
313 }
314
315 int listbox_game_id = gamelist_id_at_row_[list_i + list_rows_deleted];
316 if(game.id != listbox_game_id) {
317 ERR_LB << "Listbox game id does not match expected id "
318 << listbox_game_id << " " << game.id << " (row " << list_i << ")";
320 return;
321 }
322
323 if(game.display_status == mp::game_info::disp_status::UPDATED) {
324 LOG_LB << "Modifying game in listbox " << game.id << " (row " << list_i << ")";
326 modify_grid_with_data(grid, make_game_row_data(game));
328 ++list_i;
329 next_gamelist_id_at_row.push_back(game.id);
330 } else if(game.display_status == mp::game_info::disp_status::DELETED) {
331 LOG_LB << "Deleting game from listbox " << game.id << " (row "
332 << list_i << ")";
333 gamelistbox_->remove_row(list_i);
334 ++list_rows_deleted;
335 } else {
336 // clean
337 LOG_LB << "Clean game in listbox " << game.id << " (row " << list_i << ")";
338 next_gamelist_id_at_row.push_back(game.id);
339 ++list_i;
340 }
341 }
342 }
343
344 for(unsigned i = 0; i < next_gamelist_id_at_row.size(); ++i) {
345 if(next_gamelist_id_at_row[i] == selected_game_id_) {
346 select_row = i;
347 }
348 }
349
350 next_gamelist_id_at_row.swap(gamelist_id_at_row_);
351 if(select_row >= static_cast<int>(gamelistbox_->get_item_count())) {
352 ERR_LB << "Would select a row beyond the listbox" << select_row << " "
354 select_row = gamelistbox_->get_item_count() - 1;
355 }
356
357 if(select_row >= 0 && select_row != gamelistbox_->get_selected_row()) {
358 gamelistbox_->select_row(select_row);
359 }
360
362 gamelist_dirty_ = false;
363 last_lobby_update_ = std::chrono::steady_clock::now();
364 finish_state_sync();
366}
367
369{
370 const std::string games_string = VGETTEXT("Games: showing $num_shown out of $num_total", {
371 {"num_shown", std::to_string(lobby_info_.games_visibility().count())},
372 {"num_total", std::to_string(lobby_info_.games().size())}
373 });
374
375 gamelistbox_->find_widget<label>("map").set_label(games_string);
376
378}
379
381{
383 widget_item item;
384
385 item["use_markup"] = "true";
386
387 color_t color_string;
388 if(game.vacant_slots > 0) {
389 color_string = (game.reloaded || game.started) ? font::YELLOW_COLOR : font::GOOD_COLOR;
390 }
391
392 const std::string scenario_text = VGETTEXT("$game_name (Era: $era_name)", {
393 {"game_name", game.scenario},
394 {"era_name", game.era}
395 });
396
397 item["label"] = game.vacant_slots > 0 ? markup::span_color(color_string, game.name) : game.name;
398 data.emplace("name", item);
399
400 item["label"] = markup::span_color(font::GRAY_COLOR, game.type_marker, markup::italic(scenario_text));
401 data.emplace("scenario", item);
402
403 item["label"] = markup::span_color(color_string, game.status);
404 data.emplace("status", item);
405
406 return data;
407}
408
410{
413
414 toggle_panel& row_panel = grid->find_widget<toggle_panel>("panel");
415
416 //
417 // Game info
418 //
419 std::ostringstream ss;
420
421 const auto mark_missing = [&ss]() {
422 ss << ' ' << markup::span_color(font::BAD_COLOR, "(", _("era_or_mod^not installed"), ")");
423 };
424
425 ss << markup::tag("big", markup::span_color(font::TITLE_COLOR, _("Era"))) << "\n" << game.era;
426
427 if(!game.have_era) {
428 // NOTE: not using colorize() here deliberately to avoid awkward string concatenation.
429 mark_missing();
430 }
431
432 ss << "\n\n" << markup::tag("big", markup::span_color(font::TITLE_COLOR, _("Modifications"))) << "\n";
433
434 auto mods = game.mod_info;
435
436 if(mods.empty()) {
437 ss << _("active_modifications^None") << "\n";
438 } else {
439 for(const auto& mod : mods) {
440 ss << mod.first;
441
442 if(!mod.second) {
443 mark_missing();
444 }
445
446 ss << '\n';
447 }
448 }
449
450 // TODO: move to some general area of the code.
451 const auto yes_or_no = [](bool val) { return val ? _("yes") : _("no"); };
452
453 ss << "\n" << markup::tag("big", markup::span_color(font::TITLE_COLOR, _("Settings"))) << "\n";
454 ss << _("Experience modifier:") << " " << game.xp << "\n";
455 ss << _("Gold per village:") << " " << game.gold << "\n";
456 ss << _("Map size:") << " " << game.map_size_info << "\n";
457 ss << _("Reloaded:") << " " << yes_or_no(game.reloaded) << "\n";
458 ss << _("Shared vision:") << " " << game.vision << "\n";
459 ss << _("Shuffle sides:") << " " << yes_or_no(game.shuffle_sides) << "\n";
460 ss << _("Time limit:") << " " << game.time_limit << "\n";
461 ss << _("Use map settings:") << " " << yes_or_no(game.use_map_settings);
462
463 image& info_icon = grid->find_widget<image>("game_info");
464
465 if(!game.have_era || !game.have_all_mods || !game.required_addons.empty()) {
466 info_icon.set_label("icons/icon-info-error.png");
467
468 ss << "\n\n" << markup::span_color("#f00", markup::span_size("x-large", "! "));
469 ss << _("One or more add-ons need to be installed\nin order to join this game.");
470 } else {
471 info_icon.set_label("icons/icon-info.png");
472 }
473
474 info_icon.set_tooltip(ss.str());
475
476 //
477 // Password icon
478 //
479 image& password_icon = grid->find_widget<image>("needs_password");
480
481 if(game.password_required) {
483 } else {
485 }
486
487 //
488 // Observer icon
489 //
490 image& observer_icon = grid->find_widget<image>("observer_icon");
491
492 if(game.observers) {
493 observer_icon.set_label("misc/eye.png");
494 observer_icon.set_tooltip( _("Observers allowed"));
495 } else {
496 observer_icon.set_label("misc/no_observer.png");
497 observer_icon.set_tooltip( _("Observers not allowed"));
498 }
499
500 //
501 // Minimap
502 //
503 minimap& map = grid->find_widget<minimap>("minimap");
504
505 map.set_map_data(game.map_data);
506
507 if(!add_callbacks) {
508 return;
509 }
510
511 //
512 // Presets buttons
513 //
514 if(game.game_preset) {
515 button& delete_preset = grid->find_widget<button>("delete_preset");
516
517 connect_signal_mouse_left_click(delete_preset, std::bind(&mp_lobby::delete_preset, this, game.id));
518 } else {
520 }
521
522 //
523 // Enter game by double clicking row
524 //
525 connect_signal_mouse_left_double_click(row_panel,
526 std::bind(&mp_lobby::enter_game_by_id, this, game.id, DO_EITHER));
527}
528
529void mp_lobby::delete_preset(const int game_id)
530{
533}
534
536{
537 DBG_LB << "mp_lobby::update_gamelist_filter";
539 DBG_LB << "Games in lobby_info: " << lobby_info_.games().size()
540 << ", games in listbox: " << gamelistbox_->get_item_count();
541 assert(lobby_info_.games().size() == gamelistbox_->get_item_count());
542
544}
545
547{
548 if(delay_playerlist_update_) return;
549
550 SCOPE_LB;
551 DBG_LB << "Playerlist update: " << lobby_info_.users().size();
552
554
555 player_list_dirty_ = false;
556 last_lobby_update_ = std::chrono::steady_clock::now();
557}
558
560{
561 const int idx = gamelistbox_->get_selected_row();
562 bool can_join = false, can_observe = false;
563
564 if(idx >= 0) {
565 const mp::game_info& game = *lobby_info_.games()[idx];
566 can_observe = game.can_observe();
567 can_join = game.can_join();
569 } else {
571 }
572
573 find_widget<button>("observe_global").set_active(can_observe);
574 find_widget<button>("join_global").set_active(can_join);
575
576 player_list_dirty_ = true;
577}
578
580{
581 SCOPE_LB;
582
583 gamelistbox_ = find_widget<listbox>("game_list", false, true);
584
585 connect_signal_notify_modified(*gamelistbox_,
586 std::bind(&mp_lobby::update_selected_game, this));
587
588 player_list_.init(*this);
589
590 set_enter_disabled(true);
591
592 // Exit hook to add a confirmation when quitting the Lobby.
593 set_exit_hook(window::exit_hook::always, [this] { return get_retval() == retval::CANCEL ? quit() : true; });
594
595 chatbox_ = find_widget<chatbox>("chat", false, true);
596
598
601
602 find_widget<button>("create").set_retval(CREATE);
603
604 connect_signal_mouse_left_click(
605 find_widget<button>("show_preferences"),
607
608 connect_signal_mouse_left_click(
609 find_widget<button>("join_global"),
610 std::bind(&mp_lobby::enter_selected_game, this, DO_JOIN));
611
612 find_widget<button>("join_global").set_active(false);
613
614 connect_signal_mouse_left_click(
615 find_widget<button>("observe_global"),
616 std::bind(&mp_lobby::enter_selected_game, this, DO_OBSERVE));
617
618 connect_signal_mouse_left_click(
619 find_widget<button>("server_info"),
620 std::bind(&mp_lobby::show_server_info, this));
621
622 find_widget<button>("observe_global").set_active(false);
623
624 menu_button& replay_options = find_widget<menu_button>("replay_options");
625
626 if(prefs::get().skip_mp_replay()) {
627 replay_options.set_selected(1);
628 }
629
630 if(prefs::get().blindfold_replay()) {
631 replay_options.set_selected(2);
632 }
633
634 connect_signal_notify_modified(replay_options,
636
637 filter_text_ = find_widget<text_box>("filter_text", false, true);
638
639 connect_signal_pre_key_press(
641 std::bind(&mp_lobby::game_filter_keypress_callback, this, std::placeholders::_5));
642
643 chatbox_->room_window_open(N_("lobby"), true, false);
645
647
648 // Force first update to be directly.
651
652 // TODO: currently getting a crash in the chatbox if we use
653 // -- vultraz, 2017-11-10
654 //mp_lobby::network_handler();
655
658
659 //
660 // Profile box
661 //
662 if(panel* profile_panel = find_widget<panel>("profile", false, false)) {
663 auto your_info = std::find_if(lobby_info_.users().begin(), lobby_info_.users().end(),
664 [](const auto& u) { return u.get_relation() == mp::user_info::user_relation::ME; });
665
666 if(your_info != lobby_info_.users().end()) {
667 profile_panel->find_widget<label>("username").set_label(your_info->name);
668
669 auto& profile_button = profile_panel->find_widget<button>("view_profile");
670 connect_signal_mouse_left_click(profile_button, std::bind(&mp_lobby::open_profile_url, this));
671
672 auto& history_button = profile_panel->find_widget<button>("view_match_history");
673 connect_signal_mouse_left_click(history_button, std::bind(&mp_lobby::open_match_history, this));
674 }
675 }
676
677 listbox& tab_bar = find_widget<listbox>("games_list_tab_bar");
678 connect_signal_notify_modified(tab_bar, std::bind(&mp_lobby::tab_switch_callback, this));
679
680 // server-side queue join button
681 button* queue_join_button = find_widget<button>("join_queue", false, true);
682 connect_signal_mouse_left_click(*queue_join_button, std::bind(&mp_lobby::join_queue, this));
683
684 // server-side queue leave button
685 button* queue_leave_button = find_widget<button>("leave_queue", false, true);
686 connect_signal_mouse_left_click(*queue_leave_button, std::bind(&mp_lobby::leave_queue, this));
687
688 // server-side queues list
690
691 // Set up Lua plugin context
692 plugins_context_.reset(new plugins_context("Multiplayer Lobby"));
693
694 plugins_context_->set_callback("join", [&, this](const config&) {
696 }, true);
697
698 plugins_context_->set_callback("observe", [&, this](const config&) {
700 }, true);
701
702 plugins_context_->set_callback("create", [this](const config&) { set_retval(CREATE); }, true);
703 plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
704
705 plugins_context_->set_callback("chat", [this](const config& cfg) { chatbox_->send_chat_message(cfg["message"], false); }, true);
706 plugins_context_->set_callback("select_game", [this](const config& cfg) {
707 selected_game_id_ = cfg.has_attribute("id") ? cfg["id"].to_int() : lobby_info_.games()[cfg["index"].to_int()]->id;
708 }, true);
709
710 plugins_context_->set_accessor("game_list", [this](const config&) { return lobby_info_.gamelist(); });
711}
712
714{
715 filter_auto_hosted_ = find_widget<listbox>("games_list_tab_bar").get_selected_row() == 1;
716 filter_game_presets_ = find_widget<listbox>("games_list_tab_bar").get_selected_row() == 2;
718}
719
721{
723 if(info && info->forum_id != 0) {
725 }
726}
727
729{
730 listbox* queues_listbox = find_widget<listbox>("queue_list", false, true);
731 const std::vector<mp::queue_info>& queues = mp::get_server_queues();
732 if(queues.size() > static_cast<std::size_t>(queues_listbox->get_selected_row())) {
733 const mp::queue_info& queue = queues[queues_listbox->get_selected_row()];
734
735 std::vector<std::string> installed = installed_addons();
736 std::vector<mp::game_info::required_addon> missing;
737 for(const std::string& required : queue.required_addons) {
738 if(!utils::contains(installed, required)) {
740 req.addon_id = required;
741 req.message = required;
743 missing.emplace_back(req);
744 }
745 }
746
747 if(!missing.empty()) {
748 if(handle_addon_requirements_gui(missing, mp::game_info::addon_req::NEED_DOWNLOAD)) {
750 } else {
751 return;
752 }
753 }
754
755 mp::send_to_server(config{"join_server_queue", config{"queue_id", queue.id}});
756 } else {
757 ERR_LB << "Attempted to join queue but couldn't find queue info";
758 }
759}
760
762{
763 listbox* queues_listbox = find_widget<listbox>("queue_list", false, true);
764 const std::vector<mp::queue_info>& queues = mp::get_server_queues();
765 if(queues.size() > static_cast<std::size_t>(queues_listbox->get_selected_row())) {
766 const mp::queue_info& queue = queues[queues_listbox->get_selected_row()];
767 mp::send_to_server(config{"leave_server_queue", config{"queue_id", queue.id}});
768 } else {
769 ERR_LB << "Attempted to join queue but couldn't find queue info";
770 }
771}
772
779
781{
783 if(info) {
785 }
786}
787
789{
790 try {
791 config data;
794 }
795 } catch (const wesnothd_error& e) {
796 LOG_LB << "caught wesnothd_error in network_handler: " << e.message;
797 throw;
798 }
799
800 if(std::chrono::steady_clock::now() - last_lobby_update_ < game_config::lobby_refresh) {
801 return;
802 }
803
805 //don't process a corrupted gamelist further to prevent crashes later.
806 return;
807 }
808
812 } else {
815 }
816 }
817
821 }
822}
823
825{
826 listbox* queues_listbox = find_widget<listbox>("queue_list", false, true);
827 queues_listbox->clear();
828
829 std::vector<mp::queue_info>& queues = mp::get_server_queues();
830 std::sort(queues.begin(), queues.end(), [](const mp::queue_info& q1, const mp::queue_info& q2) { return q1.display_name < q2.display_name; });
831
832 for(const mp::queue_info& info : queues) {
834 widget_item item;
835
836 item["label"] = info.current_players.count(prefs::get().login()) > 0 ? "x" : "o";
837 data.emplace("is_in_queue", item);
838
839 item["label"] = info.display_name;
840 data.emplace("queue_name", item);
841
842 item["label"] = std::to_string(info.current_players.size())+"/"+std::to_string(info.players_required);
843 data.emplace("queue_player_count", item);
844
845 queues_listbox->add_row(data);
846 }
847}
848
850{
851 if(auto error = data.optional_child("error")) {
852 throw wesnothd_error(error["message"]);
853 } else if(data.has_child("gamelist")) {
855 } else if(auto gamelist_diff = data.optional_child("gamelist_diff")) {
856 process_gamelist_diff(*gamelist_diff);
857 } else if(auto info = data.optional_child("message")) {
858 if(info["type"] == "server_info") {
859 server_information_ = info["message"].str();
860 return;
861 } else if(info["type"] == "announcements") {
862 announcements_ = info["message"].str();
863 return;
864 }
865 } else if(auto create = data.optional_child("create_game")) {
866 queue_game_server_preset_ = create.value().mandatory_child("game");
867 queue_id_ = create["queue_id"].to_int();
869 return;
870 } else if(auto join_game = data.optional_child("join_game")) {
871 enter_game_by_id(join_game["id"].to_int(), JOIN_MODE::DO_JOIN);
872 return;
873 } else if(auto queue_update = data.optional_child("queue_update")) {
874 std::vector<mp::queue_info>& queues = mp::get_server_queues();
875 if(queue_update["action"].str() == "add") {
876 mp::queue_info& new_info = queues.emplace_back();
877 new_info.id = queue_update["queue_id"].to_int();
878 new_info.players_required = queue_update["players_required"].to_int();
879 new_info.display_name = queue_update["display_name"].str();
880 } else {
881 for(mp::queue_info& info : queues) {
882 if(info.id == queue_update["queue_id"].to_int()) {
883 if(queue_update["action"].str() == "remove") {
884 utils::erase_if(queues, [&](const mp::queue_info& i) { return i.id == queue_update["queue_id"].to_int(); });
885 } else if(queue_update["action"].str() == "update") {
886 if(queue_update->has_attribute("display_name")) {
887 info.display_name = queue_update["display_name"].str();
888 }
889 if(queue_update->has_attribute("players_required")) {
890 info.players_required = queue_update["players_required"].to_int();
891 }
892 if(queue_update->has_attribute("current_players")){
893 info.current_players = utils::split_set(queue_update["current_players"].str());
894 }
895 if(queue_update->has_attribute("addons")){
896 info.required_addons = utils::split_set(queue_update["addons"].str());
897 }
898 } else {
899 continue;
900 }
901 }
902 }
903 }
904
906
907 return;
908 }
909
911}
912
914{
916
918 DBG_LB << "Received gamelist";
919 gamelist_dirty_ = true;
920 gamelist_diff_update_ = false;
921}
922
924{
926
928 DBG_LB << "Received gamelist diff";
929 gamelist_dirty_ = true;
930 } else {
931 ERR_LB << "process_gamelist_diff failed!";
933 }
934 const int joined = data.child_count("insert_child");
935 const int left = data.child_count("remove_child");
936 if(joined > 0 || left > 0) {
937 if(left > joined) {
939 } else {
941 }
942 }
943}
944
946{
947 if(game.id < 0) {
949
950 if(preset) {
952 queue_id_ = preset["id"].to_int();
954 return;
955 }
956 }
957
958 switch(mode) {
959 case DO_JOIN:
960 if(!game.can_join()) {
961 ERR_LB << "Attempted to join a game with no vacant slots";
962 return;
963 }
964
965 break;
966 case DO_OBSERVE:
967 if(!game.can_observe()) {
968 ERR_LB << "Attempted to observe a game with observers disabled";
969 return;
970 }
971
972 break;
973 case DO_EITHER:
974 if(game.can_join()) {
975 mode = DO_JOIN;
976 } else if(game.can_observe()) {
978 } else {
979 DBG_LB << "Cannot join or observe a game.";
980 return;
981 }
982
983 break;
984 }
985
986 // prompt moderators for whether they want to join a game with observers disabled
987 if(!game.observers && mp::logged_in_as_moderator()) {
988 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) {
989 return;
990 }
991 }
992
993 const bool try_join = mode == DO_JOIN;
994 const bool try_obsv = mode == DO_OBSERVE;
995
996 // Prompt user to download this game's required addons if its requirements have not been met
997 if(game.addons_outcome != mp::game_info::addon_req::SATISFIED) {
998 if(game.required_addons.empty()) {
999 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."));
1000 return;
1001 }
1002
1003 if(!handle_addon_requirements_gui(game.required_addons, game.addons_outcome)) {
1004 return;
1005 }
1006
1007 // Addons have been downloaded, so the game_config and installed addons list need to be reloaded.
1008 // The lobby is closed and reopened.
1010 return;
1011 }
1012
1013 config join_data;
1014 join_data["id"] = std::to_string(game.id);
1015 join_data["observe"] = try_obsv;
1016
1017 if(mp::logged_in_as_moderator() && game.password_required) {
1018 if(gui2::show_message(_("Join"), _("This game is password protected. Join using moderator rights anyway?"), gui2::dialogs::message::yes_no_buttons) != gui2::retval::OK) {
1019 return;
1020 }
1021 } else if(!join_data.empty() && game.password_required) {
1022 std::string password;
1023
1024 if(!gui2::dialogs::mp_join_game_password_prompt::execute(password)) {
1025 return;
1026 }
1027
1028 join_data["password"] = password;
1029 }
1030
1031 join_data["mp_scenario"] = game.scenario_id;
1032 mp::send_to_server(config{"join", std::move(join_data)});
1033
1034 joined_game_id_ = game.id;
1035
1036 // We're all good. Close lobby and proceed to game!
1037 set_retval(try_join ? JOIN : OBSERVE);
1038}
1039
1040void mp_lobby::enter_game_by_id(const int game_id, JOIN_MODE mode)
1041{
1042 // 0 or positive is a normal game
1043 // negative is a preset
1044 mp::game_info* game_ptr = game_id >= 0
1045 ? lobby_info_.get_game_by_id(game_id)
1047
1048 if(!game_ptr) {
1049 ERR_LB << "Attempted to join/observe a game with an invalid id: " << game_id;
1050 return;
1051 }
1052
1053 enter_game(*game_ptr, mode);
1054}
1055
1060
1062{
1063 mp::send_to_server(config("refresh_lobby"));
1064}
1065
1067{
1068 if(info->game_id == 0 || info->game_id == selected_game_id_) {
1069 return;
1070 }
1071
1072 for(unsigned i = 0; i < lobby_info_.games().size(); ++i) {
1073 if(lobby_info_.games()[i]->id == info->game_id) {
1076 break;
1077 }
1078 }
1079}
1080
1082{
1083 gui2::dialogs::preferences_dialog::display();
1084
1085 refresh_lobby();
1086}
1087
1092
1094{
1096
1097 lobby_info_.add_game_filter([this](const mp::game_info& info) {
1098 for(const auto& s : utils::split(filter_text_->get_value(), ' ')) {
1099 if(!info.match_string_filter(s)) {
1100 return false;
1101 }
1102 }
1103
1104 return true;
1105 });
1106
1107 lobby_info_.add_game_filter([this](const mp::game_info& info) {
1108 return filter_friends_->get_widget_value() ? info.has_friends == true : true;
1109 });
1110
1111 // Unlike the friends filter, this is an inclusion filter (do we want to also show
1112 // games with blocked players) rather than an exclusion filter (do we want to show
1113 // only games with friends).
1114 lobby_info_.add_game_filter([this](const mp::game_info& info) {
1115 return filter_ignored_->get_widget_value() == false ? info.has_ignored == false : true;
1116 });
1117
1118 lobby_info_.add_game_filter([this](const mp::game_info& info) {
1119 return filter_slots_->get_widget_value() ? info.vacant_slots > 0 : true;
1120 });
1121
1122 lobby_info_.add_game_filter([this](const mp::game_info& info) {
1123 return info.auto_hosted == filter_auto_hosted_;
1124 });
1125
1126 lobby_info_.add_game_filter([this](const mp::game_info& info) {
1127 return info.game_preset == filter_game_presets_;
1128 });
1129
1131 [this](bool val) { return filter_invert_->get_widget_value() ? !val : val; });
1132}
1133
1135{
1136 if(key == SDLK_RETURN || key == SDLK_KP_ENTER) {
1138 }
1139}
1140
1142{
1145
1147 dlg.show();
1148
1149 if(dlg.result_open_whisper()) {
1150 lobby_chat_window* t = chatbox_->whisper_window_open(info->name, true);
1152 }
1153
1154 // Only follow a player to their game if they are actually in one.
1155 // Otherwise leave the current selection alone instead of losing it on refresh.
1156 // GitHub #1197.
1157 if(info->game_id != 0) {
1158 selected_game_id_ = info->game_id;
1159 }
1160
1161 // do not update here as it can cause issues with removing the widget
1162 // from within it's event handler. Should get updated as soon as possible
1163 // update_gamelist();
1165 player_list_dirty_ = true;
1166 refresh_lobby();
1167}
1168
1170{
1171 // TODO: this prefence should probably be controlled with an enum
1172 const int value = find_widget<menu_button>("replay_options").get_value();
1173 prefs::get().set_skip_mp_replay(value == 1);
1174 prefs::get().set_blindfold_replay(value == 2);
1175}
1176
1177} // namespace dialogs
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition manager.cpp:186
double t
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:157
bool has_attribute(std::string_view key) const
Definition config.cpp:157
bool empty() const
Definition config.cpp:823
config & mandatory_child(std::string_view key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition config.cpp:362
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:379
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:385
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:625
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:1056
void process_gamelist(const config &data)
Definition lobby.cpp:913
field_bool * filter_slots_
Definition lobby.hpp:155
virtual void post_show() override
Actions to be taken after the window has been shown.
Definition lobby.cpp:773
mp::lobby_info & lobby_info_
Definition lobby.hpp:149
text_box * filter_text_
Definition lobby.hpp:160
lobby_player_list_helper player_list_
Definition lobby.hpp:164
void skip_replay_changed_callback()
Definition lobby.cpp:1169
static std::string announcements_
Definition lobby.hpp:193
wesnothd_connection & network_connection_
Definition lobby.hpp:174
void update_gamelist_filter()
Definition lobby.cpp:535
field_bool * filter_friends_
Definition lobby.hpp:153
void process_network_data(const config &data)
Definition lobby.cpp:849
widget_data make_game_row_data(const mp::game_info &game)
Definition lobby.cpp:380
void select_game_by_player(const mp::user_info *info)
Definition lobby.cpp:1066
field_bool * filter_ignored_
Definition lobby.hpp:154
void game_filter_keypress_callback(const SDL_Keycode key)
Definition lobby.cpp:1134
field_bool * filter_invert_
Definition lobby.hpp:156
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:1040
void show_preferences_button_callback()
Definition lobby.cpp:1081
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:945
void user_dialog_callback(const mp::user_info *info)
Definition lobby.cpp:1141
mp_lobby(mp::lobby_info &info, wesnothd_connection &connection, int &joined_game)
Definition lobby.cpp:69
void process_gamelist_diff(const config &data)
Definition lobby.cpp:923
std::size_t lobby_update_timer_
Timer for updating the lobby.
Definition lobby.hpp:177
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:579
void adjust_game_row_contents(const mp::game_info &game, grid *grid, bool add_callbacks=true)
Definition lobby.cpp:409
void delete_preset(const int game_id)
Definition lobby.cpp:529
std::vector< int > gamelist_id_at_row_
Definition lobby.hpp:179
std::chrono::steady_clock::time_point last_lobby_update_
Definition lobby.hpp:170
void network_handler()
Network polling callback.
Definition lobby.cpp:788
@ RELOAD_CONFIG
player clicked the Create button
Definition lobby.hpp:65
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.
T get_widget_value()
Gets the value of the field.
Definition field.hpp:378
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:163
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:256
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition listbox.cpp:267
void remove_row(const unsigned row, unsigned count=1)
Removes a row in the listbox.
Definition listbox.cpp:108
void clear()
Removes all the rows in the listbox, clearing it.
Definition listbox.cpp:147
int get_selected_row() const
Returns the first selected row.
Definition listbox.cpp:290
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition listbox.cpp:153
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
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_visible(const visibility visible)
Definition widget.cpp:479
@ visible
The user sets the widget visible, that means:
@ invisible
The user set the widget invisible, that means:
@ hidden
The user sets the widget hidden, that means:
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition window.hpp:313
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition window.hpp:387
show_mode mode() const
Returns the dialog mode for this window.
Definition window.hpp:669
void keyboard_capture(widget *widget)
Definition window.cpp:1197
void set_exit_hook(exit_hook mode, const Func &hook)
Sets the window's exit hook.
Definition window.hpp:435
int get_retval()
Definition window.hpp:394
This class represents the collective information the client has about the players and games on the se...
void add_game_filter(game_filter_func func)
Adds a new filter function to be considered when apply_game_filter is called.
void set_game_filter_invert(std::function< bool(bool)> value)
Sets whether the result of each game filter should be inverted.
const std::vector< user_info > & users() const
bool is_game_visible(const game_info &)
Returns whether the game would be visible after the game filters are applied.
void clear_game_filters()
Clears all game filter functions.
const std::vector< game_info * > & games() const
void process_gamelist(const config &data)
Process a full game list.
bool process_gamelist_diff(const config &data)
Process a gamelist diff.
std::function< void()> begin_state_sync()
Updates the game pointer list and returns a second stage cleanup function to be called after any acti...
const boost::dynamic_bitset & games_visibility() const
game_info * get_game_by_id(int id)
Returns info on a game with the given game ID.
const config & gamelist() const
Returns the raw game list config data.
bool gamelist_initialized() const
void apply_game_filter()
Generates a new list of games that match the current filter functions and inversion setting.
static prefs & get()
void remove_game_preset(int id)
optional_const_config get_game_preset(int id)
std::string login()
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.
const config * cfg
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.
#define N_(String)
Definition gettext.hpp:108
static std::string _(const char *str)
Definition gettext.hpp:100
static lg::log_domain log_lobby("lobby")
#define LOG_LB
#define ERR_LB
#define DBG_LB
#define SCOPE_LB
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 ...
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
bool open_object(const std::string &path_or_url)
Opens the specified object with the default application configured for its type.
Definition open.cpp:50
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
std::chrono::milliseconds lobby_network_timer
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.
Definition help.cpp:84
Functions to load and save images from/to disk.
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 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::set< std::string > split_set(std::string_view s, char sep, const int flags)
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition general.hpp:88
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition general.hpp:108
std::vector< std::string > split(const config_attribute_value &val)
Desktop environment interaction functions.
int w
Definition pathfind.cpp:188
std::string_view data
Definition picture.cpp:188
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
The basic class for representing 8-bit RGB or RGBA colour values.
Definition color.hpp:51
This class represents the info a client has about a game on the server.
std::string display_name
std::set< std::string > required_addons
This class represents the information a client has about another player.
An error occurred during when trying to communicate with the wesnothd server.
static map_location::direction s
Contains the gui2 timer routines.
#define i
#define g
#define e
#define c