The Battle for Wesnoth  1.17.17+dev
match_history.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2021 - 2023
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 
16 
17 #include "desktop/open.hpp"
18 #include "formula/string_utils.hpp"
20 #include "gettext.hpp"
21 #include "filesystem.hpp"
23 #include "gui/dialogs/message.hpp"
24 #include "gui/widgets/button.hpp"
25 #include "gui/widgets/label.hpp"
26 #include "gui/widgets/listbox.hpp"
28 #include "gui/widgets/text_box.hpp"
29 #include "gui/widgets/window.hpp"
32 #include "wesnothd_connection.hpp"
33 
34 using namespace std::chrono_literals;
35 
36 static lg::log_domain log_network("network");
37 #define DBG_NW LOG_STREAM(debug, log_network)
38 #define ERR_NW LOG_STREAM(err, log_network)
39 
40 namespace gui2::dialogs
41 {
42 REGISTER_DIALOG(mp_match_history)
43 
44 mp_match_history::mp_match_history(const std::string& player_name, wesnothd_connection& connection, bool wait_for_response)
45  : modal_dialog(window_id())
46  , player_name_(player_name)
47  , connection_(connection)
48  , offset_(0)
49  , wait_for_response_(wait_for_response)
50 {
51  register_label("title", true, VGETTEXT("Match History — $player", {{"player", player_name_}}));
52 }
53 
54 void mp_match_history::pre_show(window& win)
55 {
56  button& newer_history = find_widget<button>(&win, "newer_history", false);
57  button& older_history = find_widget<button>(&win, "older_history", false);
58  connect_signal_mouse_left_click(newer_history, std::bind(&mp_match_history::newer_history_offset, this));
59  connect_signal_mouse_left_click(older_history, std::bind(&mp_match_history::older_history_offset, this));
60 
61  button& search = find_widget<button>(&win, "search", false);
62  connect_signal_mouse_left_click(search, std::bind(&mp_match_history::new_search, this));
63 
64  text_box& search_player = find_widget<text_box>(&win, "search_player", false);
65  search_player.set_value(player_name_);
66 
67  std::vector<config> content_types;
68  content_types.emplace_back("label", _("Scenario"));
69  content_types.emplace_back("label", _("Era"));
70  content_types.emplace_back("label", _("Modification"));
71 
72  find_widget<menu_button>(&win, "search_content_type", false).set_values(content_types);
73 
74  update_display();
75 }
76 
77 void mp_match_history::new_search()
78 {
79  int old_offset = offset_;
80  std::string old_player_name = player_name_;
81  text_box& search_player = find_widget<text_box>(get_window(), "search_player", false);
82  player_name_ = search_player.get_value();
83 
84  // display update failed, set the offset back to what it was before
85  if(!update_display()) {
86  offset_ = old_offset;
87  player_name_ = old_player_name;
88  } else {
89  label& title = find_widget<label>(get_window(), "title", false);
90  title.set_label(VGETTEXT("Match History — $player", {{"player", player_name_}}));
91  }
92 }
93 
94 void mp_match_history::newer_history_offset()
95 {
96  offset_ -= 10;
97  // display update failed, set the offset back to what it was before
98  if(!update_display()) {
99  offset_ += 10;
100  }
101 }
102 
103 void mp_match_history::older_history_offset()
104 {
105  offset_ += 10;
106  // display update failed, set the offset back to what it was before
107  if(!update_display()) {
108  offset_ -= 10;
109  }
110 }
111 
112 bool mp_match_history::update_display()
113 {
114  const config history = request_history();
115 
116  // request failed, nothing to do
117  if(history.child_count("game_history_results") == 0) {
118  return false;
119  }
120 
121  listbox* history_box = find_widget<listbox>(get_window(), "history", false, true);
122  history_box->clear();
123 
124  listbox* tab_bar = find_widget<listbox>(get_window(), "tab_bar", false, true);
125  connect_signal_notify_modified(*tab_bar, std::bind(&mp_match_history::tab_switch_callback, this));
126 
127  int i = 0;
128  for(const config& game : history.mandatory_child("game_history_results").child_range("game_history_result")) {
129  widget_data row;
130  grid& history_grid = history_box->add_row(row);
131 
132  dynamic_cast<label*>(history_grid.find("game_name", false))->set_label(game["game_name"].str());
133  dynamic_cast<label*>(history_grid.find("scenario_name", false))->set_label(game["scenario_name"].str());
134  dynamic_cast<label*>(history_grid.find("era_name", false))->set_label("<span color='#baac7d'>"+_("Era: ")+"</span>"+game["era_name"].str());
135  dynamic_cast<label*>(history_grid.find("game_start", false))->set_label(game["game_start"].str()+_(" UTC+0"));
136  dynamic_cast<label*>(history_grid.find("version", false))->set_label(game["version"].str());
137 
138  button* replay_download = dynamic_cast<button*>(history_grid.find("replay_download", false));
139  std::string replay_url = game["replay_url"].str();
140  if(!replay_url.empty()) {
141  std::string filename = utils::split(replay_url, '/').back();
142  std::string local_save = filesystem::get_saves_dir()+"/"+filename;
143 
144  connect_signal_mouse_left_click(*replay_download, std::bind(&network::download, replay_url, local_save));
145  } else {
146  replay_download->set_active(false);
147  }
148 
149  std::vector<std::string> player_list;
150  for(const config& player : game.child_range("player")) {
151  player_list.emplace_back(player["name"].str()+": "+player["faction"].str());
152  }
153  label* players = dynamic_cast<label*>(history_grid.find("players", false));
154  players->set_label(utils::join(player_list, "\n"));
156 
157  label* modifications = dynamic_cast<label*>(history_grid.find("modifications", false));
158  const auto& children = game.child_range("modification");
159  if(!children.empty()) {
160  std::vector<std::string> modifications_list;
161 
162  for(const config& modification : game.child_range("modification")) {
163  modifications_list.emplace_back(modification["name"].str());
164  }
165 
166  modifications->set_label(utils::join(modifications_list, "\n"));
167  }
169 
170  i++;
171  if(i == 10) {
172  break;
173  }
174  }
175 
176  // this is already the most recent history, can't get anything newer
177  if(offset_ == 0) {
178  button* newer_history = find_widget<button>(get_window(), "newer_history", false, true);
179  newer_history->set_active(false);
180  } else {
181  button* newer_history = find_widget<button>(get_window(), "newer_history", false, true);
182  newer_history->set_active(true);
183  }
184 
185  // the server returns up to 11 and the client displays at most 10
186  // if fewer than 11 rows are returned, then there are no older rows left to get next
187  if(history.child_count("game_history_result") < 11) {
188  button* older_history = find_widget<button>(get_window(), "older_history", false, true);
189  older_history->set_active(false);
190  } else {
191  button* older_history = find_widget<button>(get_window(), "older_history", false, true);
192  older_history->set_active(true);
193  }
194 
195  return true;
196 }
197 
198 const config mp_match_history::request_history()
199 {
200  config request;
201  config& child = request.add_child("game_history_request");
202  child["offset"] = offset_;
203  child["search_player"] = player_name_;
204  child["search_game_name"] = find_widget<text_box>(get_window(), "search_game_name", false).get_value();
205  child["search_content_type"] = find_widget<menu_button>(get_window(), "search_content_type", false).get_value();
206  child["search_content"] = find_widget<text_box>(get_window(), "search_content", false).get_value();
207  DBG_NW << request.debug();
208  connection_.send_data(request);
209 
210  int times_waited = 0;
211  while(true) {
212  config response;
213 
214  // I'm not really sure why this works to be honest
215  // I would've expected that there would be a risk of regular lobby responses showing up here since it's a reference to the lobby's network connection
216  // however testing has resulted in showing that this is not the case
217  // lobby responses are received in the lobby's network_handler() method when this method is not running
218  // lobby responses are not received while this method is running, and are handled in the lobby after it completes
219  // history results are never received in the lobby
220  if(connection_.receive_data(response)) {
221  if(response.child_count("game_history_results") == 0) {
222  DBG_NW << "Received non-history data: " << response.debug();
223  if(!response["error"].str().empty()) {
224  ERR_NW << "Received error from server: " << response["error"].str();
225  gui2::show_error_message(_("The server responded with an error:")+" "+response["error"].str());
226  return {};
227  }
228  } else if(response.mandatory_child("game_history_results").child_count("game_history_result") == 0) {
229  DBG_NW << "Player has no game history data.";
230  gui2::show_error_message(_("No game history found."));
231  return {};
232  } else {
233  DBG_NW << "Received history data: " << response.debug();
234  return response;
235  }
236  } else {
237  DBG_NW << "Received no data";
238  }
239 
240  if(times_waited > 20 || !wait_for_response_) {
241  ERR_NW << "Timed out waiting for history data, returning nothing";
242  if(wait_for_response_) {
243  gui2::show_error_message(_("Request timed out."));
244  }
245  return {};
246  }
247 
248  times_waited++;
249  std::this_thread::sleep_for(250ms);
250  }
251 
252  DBG_NW << "Something else happened while waiting for history data, returning nothing";
253  gui2::show_error_message(_("Request encountered an unexpected error, please check the logs."));
254  return {};
255 }
256 
257 void mp_match_history::tab_switch_callback()
258 {
259  listbox* history_box = find_widget<listbox>(get_window(), "history", false, true);
260  listbox* tab_bar = find_widget<listbox>(get_window(), "tab_bar", false, true);
261  int tab = tab_bar->get_selected_row();
262 
263  for(unsigned i = 0; i < history_box->get_item_count(); i++) {
264  grid* history_grid = history_box->get_row_grid(i);
265  if(tab == 0) {
266  dynamic_cast<label*>(history_grid->find("scenario_name", false))->set_visible(gui2::widget::visibility::visible);
267  dynamic_cast<label*>(history_grid->find("era_name", false))->set_visible(gui2::widget::visibility::visible);
268  dynamic_cast<label*>(history_grid->find("game_start", false))->set_visible(gui2::widget::visibility::visible);
269  dynamic_cast<label*>(history_grid->find("version", false))->set_visible(gui2::widget::visibility::visible);
270  dynamic_cast<button*>(history_grid->find("replay_download", false))->set_visible(gui2::widget::visibility::visible);
271  dynamic_cast<label*>(history_grid->find("players", false))->set_visible(gui2::widget::visibility::invisible);
272  dynamic_cast<label*>(history_grid->find("modifications", false))->set_visible(gui2::widget::visibility::invisible);
273  } else if(tab == 1) {
274  dynamic_cast<label*>(history_grid->find("scenario_name", false))->set_visible(gui2::widget::visibility::invisible);
275  dynamic_cast<label*>(history_grid->find("era_name", false))->set_visible(gui2::widget::visibility::invisible);
276  dynamic_cast<label*>(history_grid->find("game_start", false))->set_visible(gui2::widget::visibility::invisible);
277  dynamic_cast<label*>(history_grid->find("version", false))->set_visible(gui2::widget::visibility::invisible);
278  dynamic_cast<button*>(history_grid->find("replay_download", false))->set_visible(gui2::widget::visibility::invisible);
279  dynamic_cast<label*>(history_grid->find("players", false))->set_visible(gui2::widget::visibility::visible);
280  dynamic_cast<label*>(history_grid->find("modifications", false))->set_visible(gui2::widget::visibility::invisible);
281  } else if(tab == 2) {
282  dynamic_cast<label*>(history_grid->find("scenario_name", false))->set_visible(gui2::widget::visibility::invisible);
283  dynamic_cast<label*>(history_grid->find("era_name", false))->set_visible(gui2::widget::visibility::invisible);
284  dynamic_cast<label*>(history_grid->find("game_start", false))->set_visible(gui2::widget::visibility::invisible);
285  dynamic_cast<label*>(history_grid->find("version", false))->set_visible(gui2::widget::visibility::invisible);
286  dynamic_cast<button*>(history_grid->find("replay_download", false))->set_visible(gui2::widget::visibility::invisible);
287  dynamic_cast<label*>(history_grid->find("players", false))->set_visible(gui2::widget::visibility::invisible);
288  dynamic_cast<label*>(history_grid->find("modifications", false))->set_visible(gui2::widget::visibility::visible);
289  }
290  }
291 }
292 
293 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:161
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:371
std::size_t child_count(config_key_type key) const
Definition: config.cpp:301
child_itors child_range(config_key_type key)
Definition: config.cpp:277
std::string debug() const
Definition: config.cpp:1248
config & add_child(config_key_type key)
Definition: config.cpp:445
Simple push button.
Definition: button.hpp:37
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition: button.cpp:65
Abstract base class for all modal dialogs.
Base container class.
Definition: grid.hpp:32
widget * find(const std::string &id, const bool must_be_active) override
See widget::find.
Definition: grid.cpp:645
A label displays a text, the text can be wrapped but no scrollbars are provided.
Definition: label.hpp:58
The listbox class.
Definition: listbox.hpp:46
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:62
const grid * get_row_grid(const unsigned row) const
Returns the grid of the wanted row.
Definition: listbox.cpp:233
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:121
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:271
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition: listbox.cpp:127
virtual void set_label(const t_string &label)
std::string get_value() const
virtual void set_value(const std::string &text)
The set_value is virtual for the password_box class.
Class for a single line text area.
Definition: text_box.hpp:142
void set_visible(const visibility visible)
Definition: widget.cpp:456
@ visible
The user sets the widget visible, that means:
@ invisible
The user set the widget invisible, that means:
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:67
A class that represents a TCP/IP connection to the wesnothd server.
Declarations for File-IO.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:968
static std::string _(const char *str)
Definition: gettext.hpp:93
This file contains the window object, this object is a top level container which has the event manage...
#define ERR_NW
static lg::log_domain log_network("network")
#define DBG_NW
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
std::string get_saves_dir()
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:205
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:179
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:35
void show_error_message(const std::string &msg, bool message_use_markup)
Shows an error message to the user.
Definition: message.cpp:204
void download(const std::string &url, const std::string &local_path)
Initiates a standalone download of a single file from an HTTPS URL.
const std::vector< std::string > & modifications(bool mp)
Definition: game.cpp:711
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::vector< std::string > split(const config_attribute_value &val)
SDL_Window * get_window()
Definition: video.cpp:642
Desktop environment interaction functions.