The Battle for Wesnoth  1.19.4+dev
mp_join_game.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2024
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"
20 #include "font/text_formatting.hpp"
21 #include "formatter.hpp"
22 #include "formula/string_utils.hpp"
23 #include "game_config.hpp"
24 #include "game_config_manager.hpp"
27 #include "gettext.hpp"
28 #include "gui/core/timer.hpp"
33 #include "gui/widgets/button.hpp"
34 #include "gui/widgets/chatbox.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"
46 #include "wesnothd_connection.hpp"
47 
48 static 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 
54 namespace gui2::dialogs
55 {
56 
57 REGISTER_DIALOG(mp_join_game)
58 
59 mp_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 }
73 
75 {
76  if(update_timer_ != 0) {
78  update_timer_ = 0;
79  }
80 }
81 
82 /*
83  * Fetch the selected game's config from the server and prompts an initial faction selection.
84  */
86 {
87  // Ask for the next scenario data, if applicable
88  if(!first_scenario_) {
89  mp::send_to_server(config("load_next_scenario"));
90  }
91 
92  bool has_scenario_and_controllers = false;
93  while(!has_scenario_and_controllers) {
94  config revc;
95 
98 
100  });
101 
102  if(auto err = revc.optional_child("error")) {
103  throw wesnothd_error(err["message"]);
104  } else if(revc.has_child("leave_game")) {
105  return false;
106  } else if(auto next_scenario = revc.optional_child("next_scenario")) {
108  } else if(revc.has_attribute("version")) {
109  level_.swap(revc);
110 
111  has_scenario_and_controllers = true;
112  } else if(auto controllers = revc.optional_child("controllers")) {
113  int index = 0;
114  for(const config& controller : controllers->child_range("controller")) {
115  if(auto side = get_scenario().optional_child("side", index)) {
116  side["is_local"] = controller["is_local"];
117  }
118  ++index;
119  }
120 
121  has_scenario_and_controllers = true;
122  }
123  }
124 
125  if(level_["started"].to_bool()) {
127  return true;
128  }
129 
130  if(first_scenario_) {
131  state_.clear();
133 
134  // Make sure that we have the same config as host, if possible.
135  std::string scenario_id = state_.get_scenario_id();
136  // since add-ons are now only enabled when used, the scenario ID may still not be known
137  // so check in the MP info sent from the server for the scenario ID if that's the case
138  if(scenario_id == "") {
139  for(const auto& addon : level_.mandatory_child("multiplayer").child_range("addon")) {
140  for(const auto& content : addon.child_range("content")) {
141  if(content["type"] == "scenario") {
142  scenario_id = content["id"].str();
143  }
144  }
145  }
146  }
148  }
149 
151 
152  // If we're just an observer, we don't need to find an appropriate side and set faction selection
153  if(observe_game_) {
154  return true;
155  }
156 
157  // Search for an appropriate vacant slot. If a description is set (i.e. we're loading from a saved game),
158  // then prefer to get the side with the same description as our login. Otherwise just choose the first
159  // available side.
160  const config* side_choice = nullptr;
161 
162  int side_num_choice = 1, side_num_counter = 1;
163  for(const config& side : get_scenario().child_range("side")) {
164  // TODO: it can happen that the scenario specifies that the controller
165  // of a side should also gain control of another side.
166  if(side["controller"] == side_controller::reserved && side["current_player"] == prefs::get().login()) {
167  side_choice = &side;
168  side_num_choice = side_num_counter;
169  break;
170  }
171 
172  if(side["controller"] == side_controller::human && side["player_id"].empty()) {
173  if(!side_choice) { // Found the first empty side
174  side_choice = &side;
175  side_num_choice = side_num_counter;
176  }
177 
178  if(side["current_player"] == prefs::get().login()) {
179  side_choice = &side;
180  side_num_choice = side_num_counter;
181  break; // Found the preferred one
182  }
183  }
184 
185  if(side["player_id"] == prefs::get().login()) {
186  // We already own a side in this game
187  return true;
188  }
189 
190  ++side_num_counter;
191  }
192 
193  if(!side_choice) {
194  observe_game_ = true;
195  return true;
196  }
197 
198  // If the client is allowed to choose their team, do that here instead of having it set by the server
199  if((*side_choice)["allow_changes"].to_bool(true)) {
200  if(!show_flg_select(side_num_choice, true)) {
201  return false;
202  }
203  }
204 
205  return true;
206 }
207 
208 static std::string generate_user_description(const config& side)
209 {
210  // Allow the host to override, since only the host knows the ai_algorithm.
211  if(const config::attribute_value* desc = side.get("user_description")) {
212  return desc->str();
213  }
214 
215  const std::string controller_type = side["controller"].str();
216  const std::string reservation = side["current_player"].str();
217  const std::string owner = side["player_id"].str();
218 
219  if(controller_type == side_controller::ai) {
220  return _("Computer Player");
221  } else if(controller_type == side_controller::none) {
222  return _("Empty slot");
223  } else if(controller_type == side_controller::reserved) {
224  return VGETTEXT("Reserved for $playername", {{"playername", reservation}});
225  } else if(owner.empty()) {
226  return _("Vacant slot");
227  } else if(controller_type == side_controller::human) {
228  return owner;
229  } else {
230  return _("empty");
231  }
232 }
233 
235 {
236  set_enter_disabled(true);
237  set_escape_disabled(true);
238 
239  //
240  // Set title
241  //
242  label& title = find_widget<label>("title");
243  // FIXME: very hacky way to get the game name...
244  title.set_label((formatter() << level_.mandatory_child("multiplayer")["scenario"] << " " << font::unicode_em_dash << " " << get_scenario()["name"].t_str()).str());
245 
246  //
247  // Set up sides list
248  //
250 
251  //
252  // Initialize chatbox and game rooms
253  //
254  chatbox& chat = find_widget<chatbox>("chat");
255 
256  chat.room_window_open(N_("this game"), true, false);
257  chat.active_window_changed();
258  chat.load_log(default_chat_log, false);
259 
260  //
261  // Set up player list
262  //
263  player_list_.reset(new player_list_helper(this));
264 
265  //
266  // Set up the network handling
267  //
269 
270  //
271  // Set up the Lua plugin context
272  //
273  plugins_context_.reset(new plugins_context("Multiplayer Join"));
274 
275  plugins_context_->set_callback("launch", [this](const config&) { set_retval(retval::OK); }, false);
276  plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
277  plugins_context_->set_callback("chat", [&chat](const config& cfg) { chat.send_chat_message(cfg["message"], false); }, true);
278 }
279 
280 bool mp_join_game::show_flg_select(int side_num, bool first_time)
281 {
282  if(auto side_choice = get_scenario().optional_child("side", side_num - 1)) {
283  if(!side_choice["allow_changes"].to_bool(true)) {
284  return true;
285  }
286 
287  auto era = level_.optional_child("era");
288  if(!era) {
289  ERR_MP << "no era information";
290  return false;
291  }
292 
293  config::const_child_itors possible_sides = era->child_range("multiplayer_side");
294  if(possible_sides.empty()) {
295  WRN_MP << "no [multiplayer_side] found in era '" << era["id"] << "'.";
296  return false;
297  }
298 
299  const std::string color = side_choice["color"].str();
300 
301  std::vector<const config*> era_factions;
302  //make this safe against changes to level_ that might make possible_sides invalid pointers.
303  config era_copy;
304  for(const config& side : possible_sides) {
305  config& side_new = era_copy.add_child("multiplayer_side", side);
306  era_factions.push_back(&side_new);
307  }
308 
309  const bool is_mp = state_.classification().is_normal_mp_game();
310  const bool lock_settings = get_scenario()["force_lock_settings"].to_bool(!is_mp);
311  const bool use_map_settings = level_.mandatory_child("multiplayer")["mp_use_map_settings"].to_bool();
312  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);
313 
314  ng::flg_manager flg(era_factions, *side_choice, lock_settings, use_map_settings, saved_game == saved_game_mode::type::midgame);
315 
316  {
317  gui2::dialogs::faction_select flg_dialog(flg, color, side_num);
318  utils::guard_value guard(flg_dialog_, &flg_dialog);
319 
320  if(!flg_dialog.show() && !first_time) {
321  return true;
322  }
323  }
324 
325  config faction;
326  config& change = faction.add_child("change_faction");
327  change["change_faction"] = true;
328  change["name"] = prefs::get().login();
329  change["faction"] = flg.current_faction()["id"];
330  change["leader"] = flg.current_leader();
331  change["gender"] = flg.current_gender();
332  // TODO: the host cannot yet handle this and always uses the first side owned by that player.
333  change["side_num"] = side_num;
334 
335  mp::send_to_server(faction);
336  }
337 
338  return true;
339 }
340 
342 {
343  if(stop_updates_) {
344  return;
345  }
346 
347  tree_view& tree = find_widget<tree_view>("side_list");
348 
349  tree.clear();
350  team_tree_map_.clear();
351  const widget_data empty_map;
352 
353  int side_num = 0;
354  for(const auto& side : get_scenario().child_range("side")) {
355  ++side_num;
356  if(!side["allow_player"].to_bool(true)) {
357  continue;
358  }
359 
360  // Check to see whether we've added a toplevel tree node for this team. If not, add one
361  if(team_tree_map_.find(side["team_name"].str()) == team_tree_map_.end()) {
364 
365  item["label"] = t_string::from_serialized(side["user_team_name"]);
366  data.emplace("tree_view_node_label", item);
367 
368  tree_view_node& team_node = tree.add_node("team_header", data);
369  team_node.add_sibling("side_spacer", empty_map);
370 
371  team_tree_map_[side["team_name"].str()] = &team_node;
372  }
373 
376 
377  const std::string color = !side["color"].empty() ? side["color"] : side["side"].str();
378 
379  item["label"] = (formatter() << "<span color='" << font::get_pango_color_from_id(color) << "'>" << side["side"] << "</span>").str();
380  data.emplace("side_number", item);
381 
382  std::string leader_image = ng::random_enemy_picture;
383  std::string leader_type = side["type"];
384  std::string leader_gender = side["gender"];
385  std::string leader_name;
386 
387  // If there is a unit which can recruit, use it as a leader.
388  // Necessary to display leader information when loading saves.
389  for(const config& side_unit : side.child_range("unit")) {
390  if(side_unit["canrecruit"].to_bool()) {
391  leader_type = side_unit["type"].str();
392  leader_gender = side_unit["gender"].str();
393  break;
394  }
395  }
396 
397  if(const unit_type* ut = unit_types.find(leader_type)) {
398  const unit_type& type = ut->get_gender_unit_type(leader_gender);
399 
400  leader_image = formatter() << type.image() << "~RC(" << type.flag_rgb() << ">" << color << ")";
401  leader_name = type.type_name();
402  }
403 
404  item["label"] = leader_image;
405  data.emplace("leader_image", item);
406 
407  std::string description = generate_user_description(side);
408  if(!leader_name.empty()) {
409  description += formatter() << " (<i>" << leader_name << "</i>)";
410  }
411 
412  item["label"] = description;
413  data.emplace("leader_type", item);
414 
415  item["label"] = side["faction_name"];
416  data.emplace("leader_faction", item);
417 
418  std::string gender_icon = "icons/icon-random.png";
419  if(leader_gender != "null") {
420  gender_icon = formatter() << "icons/icon-" << leader_gender << ".png";
421  item["tooltip"] = leader_gender;
422  }
423 
424  item["label"] = gender_icon;
425  data.emplace("leader_gender", item);
426 
427  item.clear();
428 
429  // Don't show gold for saved games
430  // TODO: gold icon
431  if(side["allow_changes"].to_bool()) {
432  item["label"] = side["gold"].str() + " " + _("Gold");
433  data.emplace("side_gold", item);
434  }
435 
436  const int income_amt = side["income"];
437  if(income_amt != 0) {
438  const std::string income_string = formatter() << (income_amt > 0 ? "+" : "") << income_amt << " " << _("Income");
439 
440  item["label"] = income_string;
441  data.emplace("side_income", item);
442  }
443 
444  tree_view_node& node = team_tree_map_[side["team_name"].str()]->add_child("side_panel", data);
445 
446  grid& row_grid = node.get_grid();
447 
448  auto* select_leader_button = &row_grid.find_widget<button>("select_leader", false);
449  if(select_leader_button) {
450  if(side["player_id"] == prefs::get().login() && side["allow_changes"].to_bool(true)) {
451  //
452  // Small wrapper function in order to set the handled and halt parameters and prevent
453  // crashes in case the dialog closes and the original button to which the callback was
454  // bound had already been destroyed. The other use of show_flg_select doesn't need these
455  // parameters, so it's easier not to declare them as function arguments.
456  //
457  const auto handler = [this, side_num](bool& handled, bool& halt) {
458  show_flg_select(side_num);
459  // note: this function is called from a std::function object stored in the widget
460  // and show_flg_select which internally calls
461  // show_dialog -> pump -> ... -> network_handler -> ... -> generate_side_list
462  // might destroy that std::function object while it is executed, this means that
463  // using the captured variables 'this' and 'side_num' after this will result in
464  // unexpected behaviour or crashes.
465  handled = halt = true;
466  };
467 
468  connect_signal_mouse_left_click(*select_leader_button, std::bind(handler, std::placeholders::_3, std::placeholders::_4));
469  } else {
470  select_leader_button->set_visible(widget::visibility::hidden);
471  }
472  }
473 
474  if(income_amt == 0) {
477  }
478  }
479 }
480 
482 {
483  if(flg_dialog_) {
484  if(window* w = flg_dialog_->get_window()) {
485  w->set_retval(retval::CANCEL);
486  }
487  }
488 }
489 
491 {
492  // If the game has already started, close the dialog immediately.
493  if(level_["started"].to_bool()) {
495  return;
496  }
497 
498  config data;
500  return;
501  }
502 
503  // Update chat
504  find_widget<chatbox>("chat").process_network_data(data);
505 
506  if(!data["message"].empty()) {
507  gui2::show_transient_message(_("Response") , data["message"]);
508  }
509 
510  if(data["failed"].to_bool()) {
512 
514  } else if(data.has_child("start_game")) {
516 
517  level_["started"] = true;
519  } else if(data.has_child("leave_game")) {
521 
523  }
524 
525  if(data.has_child("stop_updates")) {
526  stop_updates_ = true;
527  } else if(auto c = data.optional_child("scenario_diff")) {
528  // TODO: We should catch config::error and then leave the game.
529  level_.apply_diff(*c);
530 
532  } else if(auto change = data.optional_child("change_controller")) {
533  if(auto side_to_change = get_scenario().find_child("side", "side", change["side"])) {
534  side_to_change->merge_with(*change);
535  }
536 
537  if(flg_dialog_ && flg_dialog_->get_side_num() == change["side"].to_int()) {
539  }
540  } else if(data.has_child("scenario") || data.has_child("snapshot") || data.has_child("next_scenario")) {
541  level_ = first_scenario_ ? data : data.mandatory_child("next_scenario");
542 
544  }
545 
546  if(data.has_child("turn")) {
547  ERR_MP << "received replay data\n" << data << "\n in mp join";
548  }
549 
550  // Update player list
551  if(data.has_child("user")) {
552  player_list_->update_list(data.child_range("user"));
553  }
554 }
555 
557 {
558  if(auto scenario = level_.optional_child("scenario")) {
559  return *scenario;
560  } else if(auto snapshot = level_.optional_child("snapshot")) {
561  return *snapshot;
562  }
563 
564  return level_;
565 }
566 
568 {
569  if(update_timer_ != 0) {
571  update_timer_ = 0;
572  }
573 
574  if(get_retval() == retval::OK) {
575 
577 
579  } else if(observe_game_) {
580  mp::send_to_server(config("observer_quit", config { "name", prefs::get().login() }));
581  } else {
582  mp::send_to_server(config("leave_game"));
583  }
584 }
585 
586 } // 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:172
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:365
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:315
bool has_attribute(config_key_type key) const
Definition: config.cpp:156
child_itors child_range(config_key_type key)
Definition: config.cpp:271
void apply_diff(const config &diff, bool track=false)
A function to apply a diff config onto this config object.
Definition: config.cpp:1030
void swap(config &cfg)
Definition: config.cpp:1340
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:296
const attribute_value * get(config_key_type key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition: config.cpp:689
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:383
config & add_child(config_key_type key)
Definition: config.cpp:439
std::ostringstream wrapper.
Definition: formatter.hpp:40
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:374
virtual void send_chat_message(const std::string &message, bool allies_only) override
Inherited form chat_handler.
Definition: chatbox.cpp:242
void load_log(std::map< std::string, chatroom_log > &log, bool show_lobby)
Definition: chatbox.cpp:89
void active_window_changed()
Definition: chatbox.cpp:104
static void progress(loading_stage stage=loading_stage::none)
Report what is being loaded to the loading screen.
static void display(std::function< void()> f)
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
int get_retval() const
Returns the cached window exit code.
std::unique_ptr< player_list_helper > player_list_
virtual void pre_show() override
Actions to be taken before showing the window.
faction_select * flg_dialog_
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)
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_node(const std::string &id, const widget_data &data, const int index=-1)
Definition: tree_view.cpp:56
NOT_DANGLING T * find_widget(const std::string &id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:736
void set_visible(const visibility visible)
Definition: widget.cpp:469
window * get_window()
Get the parent window.
Definition: widget.cpp:117
@ invisible
The user set the widget invisible, that means:
@ hidden
The user sets the widget hidden, that means:
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:61
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:325
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:397
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:338
FLG stands for faction, leader and gender.
Definition: flg_manager.hpp:30
const std::string & current_gender() const
Definition: flg_manager.hpp:71
const config & current_faction() const
Definition: flg_manager.hpp:67
const std::string & current_leader() const
Definition: flg_manager.hpp:69
static prefs & get()
std::string login()
game_classification & classification()
Definition: saved_game.hpp:56
std::string get_scenario_id() const
Definition: saved_game.cpp:678
void clear()
Definition: saved_game.cpp:813
static t_string from_serialized(const std::string &string)
Definition: tstring.hpp:153
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:1263
A single unit type that the player may recruit.
Definition: types.hpp:43
Data-based RAII scope guard.
Definition: guard_value.hpp:24
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 VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
int w
#define N_(String)
Definition: gettext.hpp:101
static std::string _(const char *str)
Definition: gettext.hpp:93
Standard logging facilities (interface).
#define WRN_MP
#define ERR_MP
static lg::log_domain log_mp_connect_engine("mp/connect/engine")
std::string get_pango_color_from_id(const std::string &id)
Returns a hex color string from a color range.
const std::string unicode_em_dash
Definition: constants.cpp:44
static void add_color_info(const game_config_view &v, bool build_defaults)
unsigned lobby_network_timer
Definition: game_config.cpp:68
static std::string generate_user_description(const config &side)
REGISTER_DIALOG(editor_edit_unit)
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:177
std::size_t add_timer(const uint32_t interval, const std::function< void(std::size_t id)> &callback, const bool repeat)
Adds a new timer.
Definition: timer.cpp:127
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:168
@ 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::pair< std::string, unsigned > item
Definition: help_impl.hpp:387
Functions to load and save images from/to disk.
logger & err()
Definition: log.cpp:304
void game_has_begun()
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::size_t index(const std::string &str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
std::string_view data
Definition: picture.cpp:178
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.
mock_char c
Contains the gui2 timer routines.
unit_type_data unit_types
Definition: types.cpp:1482