The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
mp_join_game.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2008 - 2025
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
15#define GETTEXT_DOMAIN "wesnoth-lib"
16
18
19#include "chat_log.hpp"
21#include "formatter.hpp"
23#include "game_config.hpp"
27#include "gettext.hpp"
28#include "gui/core/timer.hpp"
35#include "gui/widgets/image.hpp"
36#include "gui/widgets/label.hpp"
39#include "log.hpp"
40#include "mp_ui_alerts.hpp"
42#include "saved_game.hpp"
43#include "side_controller.hpp"
44#include "units/types.hpp"
45#include "utils/guard_value.hpp"
47
48static lg::log_domain log_mp_connect_engine("mp/connect/engine");
49#define DBG_MP LOG_STREAM(debug, log_mp_connect_engine)
50#define LOG_MP LOG_STREAM(info, log_mp_connect_engine)
51#define WRN_MP LOG_STREAM(warn, log_mp_connect_engine)
52#define ERR_MP LOG_STREAM(err, log_mp_connect_engine)
53
54namespace gui2::dialogs
55{
56
57REGISTER_DIALOG(mp_join_game)
58
59mp_join_game::mp_join_game(saved_game& state, wesnothd_connection& connection, const bool first_scenario, const bool observe_game)
60 : modal_dialog(window_id())
61 , level_()
62 , state_(state)
63 , network_connection_(connection)
64 , update_timer_(0)
65 , first_scenario_(first_scenario)
66 , observe_game_(observe_game)
67 , stop_updates_(false)
68 , player_list_(nullptr)
69 , flg_dialog_(nullptr)
70{
71 set_show_even_without_video(true);
72 set_allow_plugin_skip(false);
73}
74
82
83/*
84 * Fetch the selected game's config from the server and prompts an initial faction selection.
85 */
87{
88 // Ask for the next scenario data, if applicable
89 if(!first_scenario_) {
90 mp::send_to_server(config{"load_next_scenario"});
91 }
92
93 bool has_scenario_and_controllers = false;
94 while(!has_scenario_and_controllers) {
95 config revc;
96
99
101 });
102
103 if(auto err = revc.optional_child("error")) {
104 throw wesnothd_error(err["message"]);
105 } else if(revc.has_child("leave_game")) {
106 return false;
107 } else if(auto next_scenario = revc.optional_child("next_scenario")) {
109 } else if(revc.has_attribute("version")) {
110 level_.swap(revc);
111
112 has_scenario_and_controllers = true;
113 } else if(auto controllers = revc.optional_child("controllers")) {
114 int index = 0;
115 for(const config& controller : controllers->child_range("controller")) {
116 if(auto side = get_scenario().optional_child("side", index)) {
117 side["is_local"] = controller["is_local"];
118 }
119 ++index;
120 }
121
122 has_scenario_and_controllers = true;
123 }
124 }
125
126 if(level_["started"].to_bool()) {
128 return true;
129 }
130
131 if(first_scenario_) {
132 state_.clear();
134
135 // Make sure that we have the same config as host, if possible.
136 std::string scenario_id = state_.get_scenario_id();
137 // since add-ons are now only enabled when used, the scenario ID may still not be known
138 // so check in the MP info sent from the server for the scenario ID if that's the case
139 if(scenario_id == "") {
140 for(const auto& addon : level_.mandatory_child("multiplayer").child_range("addon")) {
141 for(const auto& content : addon.child_range("content")) {
142 if(content["type"] == "scenario") {
143 scenario_id = content["id"].str();
144 }
145 }
146 }
147 }
149 }
150
152
153 // If we're just an observer, we don't need to find an appropriate side and set faction selection
154 if(observe_game_) {
155 return true;
156 }
157
158 // Search for an appropriate vacant slot. If a description is set (i.e. we're loading from a saved game),
159 // then prefer to get the side with the same description as our login. Otherwise just choose the first
160 // available side.
161 const config* side_choice = nullptr;
162
163 int side_num_choice = 1, side_num_counter = 1;
164 for(const config& side : get_scenario().child_range("side")) {
165 // TODO: it can happen that the scenario specifies that the controller
166 // of a side should also gain control of another side.
167 if(side["controller"] == side_controller::reserved && side["current_player"] == prefs::get().login()) {
168 side_choice = &side;
169 side_num_choice = side_num_counter;
170 break;
171 }
172
173 if(side["controller"] == side_controller::human && side["player_id"].empty()) {
174 if(!side_choice) { // Found the first empty side
175 side_choice = &side;
176 side_num_choice = side_num_counter;
177 }
178
179 if(side["current_player"] == prefs::get().login()) {
180 side_choice = &side;
181 side_num_choice = side_num_counter;
182 break; // Found the preferred one
183 }
184 }
185
186 if(side["player_id"] == prefs::get().login()) {
187 // We already own a side in this game
188 return true;
189 }
190
191 ++side_num_counter;
192 }
193
194 if(!side_choice) {
195 observe_game_ = true;
196 return true;
197 }
198
199 // If the client is allowed to choose their team, do that here instead of having it set by the server
200 if((*side_choice)["allow_changes"].to_bool(true)) {
201 if(!show_flg_select(side_num_choice, true)) {
202 return false;
203 }
204 }
205
206 return true;
207}
208
209static std::string generate_user_description(const config& side)
210{
211 // Allow the host to override, since only the host knows the ai_algorithm.
212 if(const config::attribute_value* desc = side.get("user_description")) {
213 return desc->str();
214 }
215
216 const std::string controller_type = side["controller"].str();
217 const std::string reservation = side["current_player"].str();
218 // Making this string const means it can't be automatically moved when returned from this method
219 std::string owner = side["player_id"].str();
220
221 if(controller_type == side_controller::ai) {
222 return _("Computer Player");
223 } else if(controller_type == side_controller::none) {
224 return _("Empty slot");
225 } else if(controller_type == side_controller::reserved) {
226 return VGETTEXT("Reserved for $playername", {{"playername", reservation}});
227 } else if(owner.empty()) {
228 return _("Vacant slot");
229 } else if(controller_type == side_controller::human) {
230 return owner;
231 } else {
232 return _("empty");
233 }
234}
235
237{
238 set_enter_disabled(true);
240
241 //
242 // Set title
243 //
244 label& title = find_widget<label>("title");
245 // FIXME: very hacky way to get the game name...
246 title.set_label((formatter() << level_.mandatory_child("multiplayer")["scenario"] << " " << font::unicode_em_dash << " " << get_scenario()["name"].t_str()).str());
247
248 //
249 // Set up sides list
250 //
252
253 //
254 // Initialize chatbox and game rooms
255 //
256 chatbox& chat = find_widget<chatbox>("chat");
257
258 chat.room_window_open(N_("this game"), true, false);
260 chat.load_log(default_chat_log, false);
261
262 //
263 // Set up player list
264 //
265 player_list_.reset(new player_list_helper(this));
266
267 //
268 // Set up the network handling
269 //
271
272 //
273 // Set up the Lua plugin context
274 //
275 plugins_context_.reset(new plugins_context("Multiplayer Join"));
276
277 plugins_context_->set_callback("launch", [this](const config&) { set_retval(retval::OK); }, false);
278 plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
279 plugins_context_->set_callback("chat", [&chat](const config& cfg) { chat.send_chat_message(cfg["message"], false); }, true);
280}
281
282bool mp_join_game::show_flg_select(int side_num, bool first_time)
283{
284 if(auto side_choice = get_scenario().optional_child("side", side_num - 1)) {
285 if(!side_choice["allow_changes"].to_bool(true)) {
286 return true;
287 }
288
289 auto era = level_.optional_child("era");
290 if(!era) {
291 ERR_MP << "no era information";
292 return false;
293 }
294
295 config::const_child_itors possible_sides = era->child_range("multiplayer_side");
296 if(possible_sides.empty()) {
297 WRN_MP << "no [multiplayer_side] found in era '" << era["id"] << "'.";
298 return false;
299 }
300
301 const std::string color = side_choice["color"].str();
302
303 std::vector<const config*> era_factions;
304 //make this safe against changes to level_ that might make possible_sides invalid pointers.
305 config era_copy;
306 for(const config& side : possible_sides) {
307 config& side_new = era_copy.add_child("multiplayer_side", side);
308 era_factions.push_back(&side_new);
309 }
310
311 const bool is_mp = state_.classification().is_normal_mp_game();
312 const bool lock_settings = get_scenario()["force_lock_settings"].to_bool(!is_mp);
313 const bool use_map_settings = level_.mandatory_child("multiplayer")["mp_use_map_settings"].to_bool();
314 const saved_game_mode::type saved_game = saved_game_mode::get_enum(level_.mandatory_child("multiplayer")["savegame"].str()).value_or(saved_game_mode::type::no);
315
316 const auto era_info = ng::era_metadata(*era);
317 ng::flg_manager flg(era_info, era_factions, *side_choice, lock_settings, use_map_settings, saved_game == saved_game_mode::type::midgame);
318
319 {
320 gui2::dialogs::faction_select flg_dialog(flg, color, side_num);
321 utils::guard_value guard(flg_dialog_, &flg_dialog);
322
323 if(!flg_dialog.show() && !first_time) {
324 return true;
325 }
326 }
327
328 config change;
329 change["change_faction"] = true;
330 change["name"] = prefs::get().login();
331 change["faction"] = flg.current_faction()["id"];
332 change["leader"] = flg.current_leader();
333 change["gender"] = flg.current_gender();
334 // TODO: the host cannot yet handle this and always uses the first side owned by that player.
335 change["side_num"] = side_num;
336
337 mp::send_to_server(config{"change_faction", std::move(change)});
338 }
339
340 return true;
341}
342
344{
345 if(stop_updates_) {
346 return;
347 }
348
349 tree_view& tree = find_widget<tree_view>("side_list");
350
351 tree.clear();
352 team_tree_map_.clear();
353 const widget_data empty_map;
354
355 int side_num = 0;
356 for(const auto& side : get_scenario().child_range("side")) {
357 ++side_num;
358 if(!side["allow_player"].to_bool(true)) {
359 continue;
360 }
361
362 // Check to see whether we've added a toplevel tree node for this team. If not, add one
363 if(team_tree_map_.find(side["team_name"].str()) == team_tree_map_.end()) {
365 widget_item item;
366
367 item["label"] = t_string::from_serialized(side["user_team_name"]);
368 data.emplace("tree_view_node_label", item);
369
370 tree_view_node& team_node = tree.add_node("team_header", data);
371 team_node.add_sibling("side_spacer", empty_map);
372
373 team_tree_map_[side["team_name"].str()] = &team_node;
374 }
375
377 widget_item item;
378
379 const std::string color_str = !side["color"].empty() ? side["color"] : side["side"].str();
380 const auto team_color_it = game_config::team_rgb_colors.find(color_str);
381
382 if (team_color_it != game_config::team_rgb_colors.end()) {
383 item["label"] = markup::span_color(team_color_it->second[0], side["side"]);
384 } else {
385 item["label"] = side["side"].str();
386 }
387 data.emplace("side_number", item);
388
389 std::string leader_image = ng::random_enemy_picture;
390
391 const config& leader = side.child_or_empty("leader");
392 std::string leader_type = leader["type"];
393 std::string leader_gender = leader["gender"];
394 std::string leader_name;
395
396 // If there is a unit which can recruit, use it as a leader.
397 // Necessary to display leader information when loading saves.
398 for(const config& side_unit : side.child_range("unit")) {
399 if(side_unit["canrecruit"].to_bool()) {
400 leader_type = side_unit["type"].str();
401 leader_gender = side_unit["gender"].str();
402 break;
403 }
404 }
405
406 if(const unit_type* ut = unit_types.find(leader_type)) {
407 const unit_type& type = ut->get_gender_unit_type(leader_gender);
408
409 leader_image = formatter() << type.image() << "~RC(" << type.flag_rgb() << ">" << color_str << ")";
410 leader_name = type.type_name();
411 }
412
413 item["label"] = leader_image;
414 data.emplace("leader_image", item);
415
416 std::string description = generate_user_description(side);
417 if(!leader_name.empty()) {
418 description += formatter() << " (<i>" << leader_name << "</i>)";
419 }
420
421 item["label"] = description;
422 data.emplace("leader_type", item);
423
424 item["label"] = side["faction_name"].t_str();
425 data.emplace("leader_faction", item);
426
427 std::string gender_icon = "icons/icon-random.png";
428 if(leader_gender != "null") {
429 gender_icon = formatter() << "icons/icon-" << leader_gender << ".png";
430 item["tooltip"] = leader_gender;
431 }
432
433 item["label"] = gender_icon;
434 data.emplace("leader_gender", item);
435
436 item.clear();
437
438 // Don't show gold for saved games
439 // TODO: gold icon
440 if(side["allow_changes"].to_bool()) {
441 item["label"] = side["gold"].str() + " " + _("Gold");
442 data.emplace("side_gold", item);
443 }
444
445 const int income_amt = side["income"].to_int();
446 if(income_amt != 0) {
447 const std::string income_string = formatter() << (income_amt > 0 ? "+" : "") << income_amt << " " << _("Income");
448
449 item["label"] = income_string;
450 data.emplace("side_income", item);
451 }
452
453 tree_view_node& node = team_tree_map_[side["team_name"].str()]->add_child("side_panel", data);
454
455 grid& row_grid = node.get_grid();
456
457 auto* select_leader_button = &row_grid.find_widget<button>("select_leader", false);
458 if(select_leader_button) {
459 if(side["player_id"] == prefs::get().login() && side["allow_changes"].to_bool(true)) {
460 //
461 // Small wrapper function in order to set the handled and halt parameters and prevent
462 // crashes in case the dialog closes and the original button to which the callback was
463 // bound had already been destroyed. The other use of show_flg_select doesn't need these
464 // parameters, so it's easier not to declare them as function arguments.
465 //
466 const auto handler = [this, side_num](bool& handled, bool& halt) {
467 show_flg_select(side_num);
468 // note: this function is called from a std::function object stored in the widget
469 // and show_flg_select which internally calls
470 // show_dialog -> pump -> ... -> network_handler -> ... -> generate_side_list
471 // might destroy that std::function object while it is executed, this means that
472 // using the captured variables 'this' and 'side_num' after this will result in
473 // unexpected behaviour or crashes.
474 handled = halt = true;
475 };
476
477 connect_signal_mouse_left_click(*select_leader_button, std::bind(handler, std::placeholders::_3, std::placeholders::_4));
478 } else {
479 select_leader_button->set_visible(widget::visibility::hidden);
480 }
481 }
482
483 if(income_amt == 0) {
486 }
487 }
488}
489
496
498{
499 // If the game has already started, close the dialog immediately.
500 if(level_["started"].to_bool()) {
502 return;
503 }
504
505 config data;
507 return;
508 }
509
510 // Update chat
511 find_widget<chatbox>("chat").process_network_data(data);
512
513 if(!data["message"].empty()) {
514 gui2::show_transient_message(_("Response") , data["message"]);
515 }
516
517 if(data["failed"].to_bool()) {
519
521 } else if(data.has_child("start_game")) {
523
524 level_["started"] = true;
526 } else if(data.has_child("leave_game")) {
528
530 }
531
532 if(data.has_child("stop_updates")) {
533 stop_updates_ = true;
534 } else if(auto c = data.optional_child("scenario_diff")) {
535 // TODO: We should catch config::error and then leave the game.
537
539 } else if(auto change = data.optional_child("change_controller")) {
540 if(auto side_to_change = get_scenario().find_child("side", "side", change["side"])) {
541 side_to_change->merge_with(*change);
542 }
543
544 if(flg_dialog_ && flg_dialog_->get_side_num() == change["side"].to_int()) {
546 }
547 } else if(data.has_child("scenario") || data.has_child("snapshot") || data.has_child("next_scenario")) {
548 level_ = first_scenario_ ? data : data.mandatory_child("next_scenario");
549
551 }
552
553 if(data.has_child("turn")) {
554 ERR_MP << "received replay data\n" << data << "\n in mp join";
555 }
556
557 // Update player list
558 if(data.has_child("user")) {
559 player_list_->update_list(data.child_range("user"));
560 }
561}
562
564{
565 if(auto scenario = level_.optional_child("scenario")) {
566 return *scenario;
567 } else if(auto snapshot = level_.optional_child("snapshot")) {
568 return *snapshot;
569 }
570
571 return level_;
572}
573
575{
576 if(update_timer_ != 0) {
578 update_timer_ = 0;
579 }
580
581 if(get_retval() == retval::OK) {
584 } else {
585 mp::send_to_server(config{"leave_game"});
586 }
587}
588
589} // namespace dialogs
std::map< std::string, chatroom_log > default_chat_log
Definition chat_log.cpp:18
Variant for storing WML attributes.
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
config & add_child(std::string_view key)
Definition config.cpp:436
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition config.cpp:380
child_itors child_range(std::string_view key)
Definition config.cpp:268
const attribute_value * get(std::string_view key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition config.cpp:665
void swap(config &cfg) noexcept
Definition config.cpp:1280
void apply_diff(const config &diff, bool track=false)
A function to apply a diff config onto this config object.
Definition config.cpp:1000
bool has_attribute(std::string_view key) const
Definition config.cpp:157
bool has_child(std::string_view key) const
Determine whether a config has a child or not.
Definition config.cpp:312
const config & child_or_empty(std::string_view key) const
Returns the first child with the given key, or an empty config if there is none.
Definition config.cpp:390
boost::iterator_range< const_child_iterator > const_child_itors
Definition config.hpp:281
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
std::ostringstream wrapper.
Definition formatter.hpp:40
std::string str() const
Definition formatter.hpp:61
static game_config_manager * get()
void load_game_config_for_game(const game_classification &classification, const std::string &scenario_id)
static game_config_view wrap(const config &cfg)
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
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
static void progress(loading_stage stage=loading_stage::none)
Report what is being loaded to the loading screen.
static void display(const std::function< void()> &f)
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
std::unique_ptr< player_list_helper > player_list_
virtual void pre_show() override
Actions to be taken before showing the window.
std::map< std::string, tree_view_node * > team_tree_map_
virtual void post_show() override
Actions to be taken after the window has been shown.
bool show_flg_select(int side_num, bool first_time=false)
void close_faction_select_dialog_if_open()
Will close the Faction Select dialog if it's open.
wesnothd_connection & network_connection_
std::unique_ptr< plugins_context > plugins_context_
Base container class.
Definition grid.hpp:32
virtual void set_label(const t_string &text)
bool empty() const
Does the node have children?
tree_view_node & add_sibling(const std::string &id, const widget_data &data)
Adds a sibling for a node at the end of the list.
tree_view_node & add_child(const std::string &id, const widget_data &data, const int index=-1)
Constructs a new child node.
tree_view_node & add_node(const std::string &id, const widget_data &data, const int index=-1)
Definition tree_view.cpp:57
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
@ 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
static const std::string & type()
Static type getter that does not rely on the widget being constructed.
void set_escape_disabled(const bool escape_disabled)
Disable the escape key.
Definition window.hpp:326
int get_retval()
Definition window.hpp:394
FLG stands for faction, leader and gender.
const std::string & current_gender() const
const std::string & current_leader() const
const config & current_faction() const
static prefs & get()
std::string login()
std::string get_scenario_id() const
game_classification & classification()
static t_string from_serialized(const std::string &string)
Definition tstring.hpp:165
const unit_type * find(const std::string &key, unit_type::BUILD_STATUS status=unit_type::FULL) const
Finds a unit_type by its id() and makes sure it is built to the specified level.
Definition types.cpp:1253
A single unit type that the player may recruit.
Definition types.hpp:43
const unit_type & get_gender_unit_type(const std::string &gender) const
Returns a gendered variant of this unit_type.
Definition types.cpp:416
Data-based RAII scope guard.
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.
bool wait_and_receive_data(config &data)
Unlike receive_data, waits until data is available instead of returning immediately.
#define WRN_MP
#define ERR_MP
const config * cfg
#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
Standard logging facilities (interface).
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
static lg::log_domain log_mp_connect_engine("mp/connect/engine")
const std::string unicode_em_dash
Definition constants.cpp:44
static void add_color_info(const game_config_view &v, bool build_defaults)
std::chrono::milliseconds lobby_network_timer
std::map< std::string, std::vector< color_t >, std::less<> > team_rgb_colors
For colors defined by WML [color_range] tags, the palettes for team-based recoloring.
static std::string generate_user_description(const config &side)
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_transient_message(const std::string &title, const std::string &message, const std::string &image, const bool message_use_markup, const bool title_use_markup)
Shows a transient message to the user.
bool remove_timer(const std::size_t id)
Removes a timer.
Definition timer.cpp:164
@ OK
Dialog was closed with the OK button.
Definition retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition retval.hpp:38
Functions to load and save images from/to disk.
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition markup.hpp:110
void level_to_gamestate(const config &level, saved_game &state)
void send_to_server(const config &data)
Attempts to send given data to server if a connection is open.
const std::string random_enemy_picture("units/random-dice.png")
std::string_view data
Definition picture.cpp:188
static constexpr utils::optional< enum_type > get_enum(const std::string_view value)
Converts a string into its enum equivalent.
Definition enum_base.hpp:57
An error occurred during when trying to communicate with the wesnothd server.
Contains the gui2 timer routines.
unit_type_data unit_types
Definition types.cpp:1499
#define c