The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
mp_staging.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 "ai/configuration.hpp"
20#include "chat_log.hpp"
23#include "formatter.hpp"
24#include "game_config.hpp"
25#include "gettext.hpp"
31#include "gui/widgets/image.hpp"
32#include "gui/widgets/label.hpp"
39#include "mp_ui_alerts.hpp"
40#include "units/types.hpp"
42
43namespace gui2::dialogs
44{
45
46REGISTER_DIALOG(mp_staging)
47
48mp_staging::mp_staging(ng::connect_engine& connect_engine, wesnothd_connection* connection)
49 : modal_dialog(window_id())
50 , connect_engine_(connect_engine)
51 , ai_algorithms_(ai::configuration::get_available_ais())
52 , network_connection_(connection)
53 , update_timer_(0)
54 , state_changed_(false)
55 , team_tree_map_()
56 , side_tree_map_()
57 , player_list_(nullptr)
58{
59 set_show_even_without_video(true);
60
61 assert(!ai_algorithms_.empty());
62}
63
71
73{
76
77 // Ctrl+G triggers 'I'm Ready' (ok) button's functionality
78 register_hotkey(hotkey::HOTKEY_MP_START_GAME, [this](auto&&...) { start_game(); return true; });
79
81 find_widget<button>("ok").set_tooltip(VGETTEXT("Hotkey(s): $bindings", {{ "bindings", bindings }}));
82
83 //
84 // Set title and status widget states
85 //
86 label& title = find_widget<label>("title");
87 title.set_label((formatter() << connect_engine_.params().name << " " << font::unicode_em_dash << " " << connect_engine_.scenario()["name"].t_str()).str());
88
90
91 //
92 // Set up sides list
93 //
94 for(const auto& side : connect_engine_.side_engines()) {
95 if(side->allow_player() || game_config::debug) {
96 add_side_node(side);;
97 }
98 }
99
100 //
101 // Initialize chatbox and game rooms
102 //
103 chatbox& chat = find_widget<chatbox>("chat");
104
105 chat.room_window_open(N_("this game"), true, false);
107 chat.load_log(default_chat_log, false);
108
109 //
110 // Set up player list
111 //
112 player_list_.reset(new player_list_helper(this));
113
114 //
115 // Set up the network handling
116 //
118
119 //
120 // Set up the Lua plugin context
121 //
122 plugins_context_.reset(new plugins_context("Multiplayer Staging"));
123
124 plugins_context_->set_callback("launch", [this](const config&) { set_retval(retval::OK); }, false);
125 plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
126 plugins_context_->set_callback("chat", [&chat](const config& cfg) { chat.send_chat_message(cfg["message"], false); }, true);
127}
128
130{
131 int position = 0;
132 for(const auto& side_engine : connect_engine_.side_engines()) {
133 if(side->team() == side_engine->team() && side->index() > side_engine->index()) {
134 ++position;
135 }
136 }
137
138 return position;
139}
140
141template<typename... T>
143{
144 static const widget_data empty_map;
145
146 // If there is no team node in the map, this will return nullptr
147 tree_view_node* team_node = team_tree_map_[side->team_name()];
148
149 // Add a team node if none exists
150 if(team_node == nullptr) {
151 tree_view& tree = find_widget<tree_view>("side_list");
152
153 widget_data tree_data;
154 widget_item tree_item;
155
156 tree_item["label"] = side->user_team_name();
157 tree_data.emplace("tree_view_node_label", tree_item);
158
159 team_node = &tree.add_node("team_header", tree_data);
160 team_node->add_sibling("side_spacer", empty_map);
161
162 team_tree_map_[side->team_name()] = team_node;
163 }
164
165 assert(team_node && "No team node found!");
166 return team_node->add_child(std::forward<T>(params)...);
167}
168
170{
172 widget_item item;
173
174 item["label"] = std::to_string(side->index() + 1);
175 data.emplace("side_number", item);
176
177 // TODO: don't hardcode magenta?
178 item["label"] = "units/unknown-unit.png~RC(magenta>" + side->color_id() + ")";
179 data.emplace("leader_image", item);
180
181 item["label"] = "icons/icon-random.png";
182 data.emplace("leader_gender", item);
183
184 tree_view_node& node = add_side_to_team_node(side, "side_panel", data, get_side_node_position(side));
185
186 side_tree_map_[side] = &node;
187
188 grid& row_grid = node.get_grid();
189
190 update_leader_display(side, row_grid);
191
192 // Status variables
193 const bool fls = connect_engine_.force_lock_settings();
194 const bool ums = connect_engine_.params().use_map_settings;
195
196 const bool lock_gold = side->cfg()["gold_lock"].to_bool(fls);
197 const bool lock_income = side->cfg()["income_lock"].to_bool(fls);
198 const bool lock_team = side->cfg()["team_lock"].to_bool(fls);
199 const bool lock_color = side->cfg()["color_lock"].to_bool(fls);
200
201 const bool saved_game = connect_engine_.params().saved_game == saved_game_mode::type::midgame;
202
203 //
204 // AI Algorithm
205 //
206 int selection = 0;
207
208 // We use an index-based loop in order to get the index of the selected option
209 std::vector<config> ai_options;
210 // If this is loading a saved game, we add an option at the beginning of the menu.
211 // This results in a mismatch between the indices of ai_options and ai_algorithms_
212 // that we need to account for later.
213 if(saved_game) {
214 ai_options.emplace_back("label", "Keep saved AI");
215 }
216 for(unsigned i = 0; i < ai_algorithms_.size(); ++i) {
217 ai_options.emplace_back("label", ai_algorithms_[i]->text);
218
219 if(ai_algorithms_[i]->id == side->ai_algorithm()) {
220 selection = i;
221 }
222 }
223
224 menu_button& ai_selection = row_grid.find_widget<menu_button>("ai_controller");
225
226 ai_selection.set_values(ai_options, selection);
227
228 connect_signal_notify_modified(ai_selection,
229 std::bind(&mp_staging::on_ai_select, this, side, std::ref(ai_selection), saved_game));
230
231 on_ai_select(side, ai_selection, saved_game);
232 //
233 // Controller
234 //
235 std::vector<config> controller_names;
236 for(const auto& controller : side->controller_options()) {
237 controller_names.emplace_back("label", controller.second);
238 }
239
240 menu_button& controller_selection = row_grid.find_widget<menu_button>("controller");
241
242 controller_selection.set_values(controller_names, side->current_controller_index());
243 controller_selection.set_active(controller_names.size() > 1);
244
245 connect_signal_notify_modified(controller_selection,
246 std::bind(&mp_staging::on_controller_select, this, side, std::ref(row_grid)));
247
248 on_controller_select(side, row_grid);
249
250 //
251 // Leader controls
252 //
253 button& leader_select = row_grid.find_widget<button>("select_leader");
254
255 //todo: shouldn't this also be disabled when the flg settings are locked.
256 leader_select.set_active(!saved_game);
257
258 connect_signal_mouse_left_click(leader_select,
259 std::bind(&mp_staging::select_leader_callback, this, side, std::ref(row_grid)));
260
261 //
262 // Team
263 //
264 std::vector<config> team_names;
265 unsigned initial_team_selection = 0;
266
267 for(unsigned i = 0; i < connect_engine_.team_data().size(); ++i) {
269
270 if(!tdata.is_player_team && !game_config::debug) {
271 continue;
272 }
273
274 config entry;
275 entry["label"] = t_string::from_serialized(tdata.user_team_name);
276
277 // Since we're not necessarily displaying every every team, we need to store the
278 // index a displayed team has in the connect_engine's team_data vector. This is
279 // then utilized in the click callback.
280 entry["team_index"] = i;
281
282 team_names.push_back(std::move(entry));
283
284 // Since, again, every team might not be displayed, and side_engine::team() returns
285 // an index into the team_data vector, get an initial selection index for the menu
286 // adjusted for the displayed named.
287 if(side->team() == i) {
288 initial_team_selection = team_names.size() - 1;
289 }
290 }
291
292 menu_button& team_selection = row_grid.find_widget<menu_button>("side_team");
293
294 team_selection.set_values(team_names, initial_team_selection);
295 //todo: shouldn't this also be disabled when team settings are locked.
296 team_selection.set_active(!saved_game);
297
298 connect_signal_notify_modified(team_selection,
299 std::bind(&mp_staging::on_team_select, this, side, std::ref(team_selection)));
300
301 //
302 // Colors
303 //
304 std::vector<config> color_options;
305 for(const auto& color_opt : side->color_options()) {
306 auto name = game_config::team_rgb_name.find(color_opt);
307 auto color = game_config::team_rgb_range.find(color_opt);
308 auto team_color = _("Invalid Color");
309
311 team_color = markup::span_color(color->second.font_color(), name->second);
312 }
313
314 // The name will be in the font_color shade, but the icon will be in the mid() shade, like a recolored unit
315 color_options.emplace_back(
316 "label", team_color,
317 "icon", (formatter() << "misc/status.png~RC(magenta>" << color_opt << ")").str()
318 );
319 }
320
321 menu_button& color_selection = row_grid.find_widget<menu_button>("side_color");
322
323 color_selection.set_values(color_options, side->color());
324 color_selection.set_active(!saved_game);
325 color_selection.set_use_markup(true);
326
327 connect_signal_notify_modified(color_selection,
328 std::bind(&mp_staging::on_color_select, this, side, std::ref(row_grid)));
329
330 //
331 // Gold and Income
332 //
333 const auto slider_setup_helper = [](slider& slider, const int value) {
334 // For the gold and income sliders, the usual min and max values are set in
335 // the dialog WML. However, if a side specifies a value out of that range,
336 // we adjust the bounds to accommodate it.
338 std::min(value, slider.get_minimum_value()),
339 std::max(value, slider.get_maximum_value())
340 );
341
343 };
344
345 slider& slider_gold = row_grid.find_widget<slider>("side_gold_slider");
346 slider_setup_helper(slider_gold, side->gold());
347
348 connect_signal_notify_modified(slider_gold, std::bind(
349 &mp_staging::on_side_slider_change<&ng::side_engine::set_gold>, this, side, std::ref(slider_gold)));
350
351 slider& slider_income = row_grid.find_widget<slider>("side_income_slider");
352 slider_setup_helper(slider_income, side->income());
353
354 connect_signal_notify_modified(slider_income, std::bind(
355 &mp_staging::on_side_slider_change<&ng::side_engine::set_income>, this, side, std::ref(slider_income)));
356
357 // TODO: maybe display the saved values
358 if(saved_game) {
361 }
362
363 //
364 // Gold, income, team, and color are only suggestions unless explicitly locked
365 //
366 if(!saved_game && ums) {
367 team_selection.set_active(!lock_team);
368 color_selection.set_active(!lock_color);
369
370 slider_gold.set_active(!lock_gold);
371 slider_income.set_active(!lock_income);
372 }
373}
374
376{
377 menu_button& ai_selection = row_grid.find_widget<menu_button>("ai_controller");
378 menu_button& controller_selection = row_grid.find_widget<menu_button>("controller");
379
380 if(side->controller_changed(controller_selection.get_value())) {
382
384 }
385}
386
388{
389 // If this is a saved game, we need to reduce the index by one, to account for
390 // the "Keep saved AI" option having been added to the computer player menu
391 int i = ai_menu.get_value();
392 if(saved_game) {
393 i--;
394 }
395
396 if(i < 0) {
397 side->set_ai_algorithm("use_saved");
398 } else {
399 side->set_ai_algorithm(ai_algorithms_[i]->id);
400 }
401
403}
404
406{
407 side->set_color(row_grid.find_widget<menu_button>("side_color").get_value());
408
409 update_leader_display(side, row_grid);
410
412}
413
415{
416 // Since we're not necessarily displaying every every team in the menu, we can't just
417 // use the selected index to set a side's team. Instead, we grab the index we stored
418 // in add_side_node from the selected config, which should correspond to the
419 // appropriate entry in the connect_engine's team name vector.
420 const unsigned team_index = team_menu.get_value_config()["team_index"].to_unsigned();
421
422 if(team_index == side->team()) {
423 return;
424 }
425
426 // Note the old team so we can remove the node if empty after the side move.
427 // Do this *before* setting the new team!
428 const std::string old_team = side->team_name();
429 side->set_team(team_index);
430
431 auto& tree = find_widget<tree_view>("side_list");
432
433 // First, remove the node from the tree
434 auto node = tree.remove_node(side_tree_map_[side]);
435
436 // Then add a new node as a child to the appropriate team's node
437 add_side_to_team_node(side, std::move(node.first), get_side_node_position(side));
438
439 tree_view_node* old_team_node = team_tree_map_[old_team];
440
441 // Last, remove the old team node if it's now empty
442 if(old_team_node->empty()) {
443 // Decor node will be immediately after team node. Remove this first!
444 tree.remove_node(old_team_node->get_node_below());
445 tree.remove_node(old_team_node);
446
447 team_tree_map_[old_team] = nullptr;
448 }
449
451}
452
454{
455 if(gui2::dialogs::faction_select::execute(side->flg(), side->color_id(), side->index() + 1)) {
456 update_leader_display(side, row_grid);
457
459 }
460}
461
462template<void(ng::side_engine::*fptr)(int)>
464{
465 std::invoke(fptr, side, slider.get_value());
466
468}
469
471{
472 // BIG FAT TODO: get rid of this shitty "null" string value in the FLG manager
473 std::string current_leader = side->flg().current_leader() != "null" ? side->flg().current_leader() : font::unicode_em_dash;
474 const std::string current_gender = side->flg().current_gender() != "null" ? side->flg().current_gender() : font::unicode_em_dash;
475
476 // Sprite
477 std::string new_image;
478
479 if(side->flg().is_random_faction() || current_leader == "random") {
480 new_image = ng::random_enemy_picture;
481 }
482
483 if(const unit_type* ut = unit_types.find(current_leader)) {
484 const unit_type& type = ut->get_gender_unit_type(current_gender);
485
486 new_image = formatter() << type.image() << "~RC(magenta>" << side->color_id() << ")";
487
488 // We don't need the unit type id anymore, and can now replace this variable with the type name
489 current_leader = type.type_name();
490 }
491
492 row_grid.find_widget<drawing>("leader_image").set_label(new_image);
493
494 // Faction and leader
495 if(!side->cfg()["name"].empty()) {
496 current_leader = formatter() << side->cfg()["name"] << " (" << markup::italic(current_leader) << ")";
497 }
498
499 row_grid.find_widget<label>("leader_type").set_label(current_leader == "random" ? _("Random") : current_leader);
500 row_grid.find_widget<label>("leader_faction").set_label(side->flg().current_faction()["name"].t_str());
501
502 // Gender
503 if(current_gender != font::unicode_em_dash) {
504 const std::string gender_icon = formatter() << "icons/icon-" << current_gender << ".png";
505
506 image& icon = row_grid.find_widget<image>("leader_gender");
507 icon.set_label(gender_icon);
508 }
509}
510
512{
513 find_widget<label>("status_label").set_label(
515 ? _("Waiting for players to join...")
516 : _("Waiting for players to choose factions...")
517 );
518
519 find_widget<button>("ok").set_active(connect_engine_.can_start_game());
520}
521
523{
524 // First, send off any changes if they've been accumulated
525 if(state_changed_) {
527 }
528
529 // Next, check for any incoming changes
530 config data;
532 return;
533 }
534
535 // Update chat
536 find_widget<chatbox>("chat").process_network_data(data);
537
538 // TODO: why is this needed...
539 const bool was_able_to_start = connect_engine_.can_start_game();
540 const bool quit_signal_received = connect_engine_.process_network_data(data);
541
542 if(quit_signal_received) {
544 }
545
546 // Update side leader displays
547 // This is basically only needed when a new player joins and selects their faction
548 for(auto& tree_entry : side_tree_map_) {
549 ng::side_engine_ptr side = tree_entry.first;
550
551 grid& row_grid = tree_entry.second->get_grid();
552
553 update_leader_display(side, row_grid);
554
555 std::vector<config> controller_names;
556 for(const auto& controller : side->controller_options()) {
557 controller_names.emplace_back("label", controller.second);
558 }
559
560 menu_button& controller_selection = row_grid.find_widget<menu_button>("controller");
561
562 controller_selection.set_values(controller_names, side->current_controller_index());
563 controller_selection.set_active(controller_names.size() > 1);
564 }
565
566 // Update player list
567 if(data.has_child("user")) {
568 player_list_->update_list(data.child_range("user"));
569 }
570
571 // Update status label and buttons
573
574 if(!was_able_to_start && connect_engine_.can_start_game()) {
576 }
577
578 state_changed_ = false;
579}
580
582{
583 if(update_timer_ != 0) {
585 update_timer_ = 0;
586 }
587
588 if(get_retval() == retval::OK) {
590 } else {
592 }
593}
594
595} // namespace dialogs
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
std::ostringstream wrapper.
Definition formatter.hpp:40
Simple push button.
Definition button.hpp:36
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition button.cpp:64
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
Abstract base class for all modal dialogs.
void update_leader_display(const ng::side_engine_ptr &side, grid &row_grid)
tree_view_node & add_side_to_team_node(const ng::side_engine_ptr &side, T &&... params)
wesnothd_connection * network_connection_
void on_color_select(const ng::side_engine_ptr &side, grid &row_grid)
virtual void post_show() override
Actions to be taken after the window has been shown.
void select_leader_callback(const ng::side_engine_ptr &side, grid &row_grid)
void on_ai_select(const ng::side_engine_ptr &side, menu_button &ai_menu, const bool saved_game)
ng::connect_engine & connect_engine_
int get_side_node_position(const ng::side_engine_ptr &side) const
Find an appropriate position to insert a side node.
void add_side_node(const ng::side_engine_ptr &side)
void on_team_select(const ng::side_engine_ptr &side, menu_button &team_menu)
std::map< ng::side_engine_ptr, tree_view_node * > side_tree_map_
std::unique_ptr< player_list_helper > player_list_
std::vector< ai::description * > ai_algorithms_
std::map< std::string, tree_view_node * > team_tree_map_
virtual void pre_show() override
Actions to be taken before showing the window.
void on_side_slider_change(const ng::side_engine_ptr &side, slider &slider)
void on_controller_select(const ng::side_engine_ptr &side, grid &row_grid)
std::unique_ptr< plugins_context > plugins_context_
void register_hotkey(const hotkey::HOTKEY_COMMAND id, Func &&function)
Registers a hotkey.
Base container class.
Definition grid.hpp:32
void set_values(const std::vector<::config > &values, unsigned selected=0)
virtual unsigned get_value() const override
Inherited from selectable_item.
const ::config & get_value_config() const
Returns the entire config object for the selected row.
virtual void set_active(const bool active) override
See styled_widget::set_active.
virtual void set_active(const bool active) override
See styled_widget::set_active.
virtual int get_minimum_value() const override
Inherited from integer_selector.
Definition slider.hpp:58
virtual void set_value(int value) override
Inherited from integer_selector.
Definition slider.cpp:81
virtual int get_maximum_value() const override
Inherited from integer_selector.
Definition slider.hpp:64
void set_value_range(int min_value, int max_value)
Definition slider.cpp:249
virtual int get_value() const override
Inherited from integer_selector.
Definition slider.hpp:52
virtual void set_label(const t_string &text)
virtual void set_use_markup(bool use_markup)
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 * get_node_below()
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
@ 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
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
const mp_game_settings & params() const
bool sides_available() const
bool force_lock_settings() const
const std::vector< side_engine_ptr > & side_engines() const
const std::vector< team_data_pod > & team_data() const
bool process_network_data(const config &data)
bool can_start_game() const
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
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.
Managing the AIs configuration - headers.
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
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
A small explanation about what's going on here: Each action has access to two game_info objects First...
Definition actions.cpp:59
const std::string unicode_em_dash
Definition constants.cpp:44
std::map< std::string, color_range, std::less<> > team_rgb_range
Colors defined by WML [color_range] tags.
std::map< std::string, t_string, std::less<> > team_rgb_name
const bool & debug
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
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
std::string get_names(const std::string &id)
Returns a comma-separated string of hotkey names.
const hotkey_command & get_hotkey_command(std::string_view command)
Returns the hotkey_command with the given id.
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
@ CNTR_COMPUTER
std::shared_ptr< side_engine > side_engine_ptr
const std::string random_enemy_picture("units/random-dice.png")
std::string_view data
Definition picture.cpp:188
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
saved_game_mode::type saved_game
unit_type_data unit_types
Definition types.cpp:1499
#define i